packages/docs/docs/studio/make-component-interactive.mdx
Interactive components can be selected in the Remotion Studio timeline and can expose editable props.
If you only need an editable HTML or SVG element, use Interactive. For a custom component, wrap it with Interactive.withSchema().
An InteractivitySchema describes which props the Studio may edit.
Use Interactive.baseSchema for props inherited from <Sequence>, such as from, trimBefore, durationInFrames, freeze, hidden, name and showInTimeline.
Use Interactive.transformSchema if your component accepts a style prop and applies it to the rendered element.
import type React from 'react';
import {
Interactive,
type InteractiveBaseProps,
type InteractiveTransformProps,
type InteractivitySchema,
} from 'remotion';
type BadgeProps = InteractiveBaseProps &
InteractiveTransformProps & {
readonly children?: React.ReactNode;
readonly color?: string;
readonly padding?: number;
};
const badgeSchema = {
...Interactive.baseSchema,
color: {
type: 'color',
default: '#0b84ff',
description: 'Color',
},
padding: {
type: 'number',
min: 0,
step: 1,
default: 16,
description: 'Padding',
hiddenFromList: false,
},
...Interactive.transformSchema,
} as const satisfies InteractivitySchema;
controlsThe inner component receives a controls prop from Interactive.withSchema().
Forward it to the <Sequence> that represents your component in the timeline.
import React, {forwardRef, useImperativeHandle, useRef} from 'react';
import {
Interactive,
Sequence,
type InteractiveBaseProps,
type InteractiveTransformProps,
type InteractivitySchema,
type SequenceControls,
} from 'remotion';
type BadgeProps = InteractiveBaseProps &
InteractiveTransformProps & {
readonly children?: React.ReactNode;
readonly color?: string;
readonly padding?: number;
};
const badgeSchema = {
...Interactive.baseSchema,
color: {
type: 'color',
default: '#0b84ff',
description: 'Color',
},
padding: {
type: 'number',
min: 0,
step: 1,
default: 16,
description: 'Padding',
hiddenFromList: false,
},
...Interactive.transformSchema,
} as const satisfies InteractivitySchema;
// ---cut---
const BadgeInner = forwardRef<
HTMLDivElement,
BadgeProps & {
readonly controls: SequenceControls | undefined;
}
>(
(
{
children,
color = '#0b84ff',
padding = 16,
style,
name,
controls,
...sequenceProps
},
ref,
) => {
const outlineRef = useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => outlineRef.current as HTMLDivElement, []);
return (
<Sequence
layout="none"
{...sequenceProps}
name={name ?? '<Badge>'}
controls={controls}
outlineRef={outlineRef}
>
<div
ref={outlineRef}
style={{
...style,
display: 'inline-flex',
borderRadius: 999,
backgroundColor: color,
color: 'white',
fontWeight: 700,
padding,
}}
>
{children}
</div>
</Sequence>
);
},
);
The exported component should not expose controls as a public prop.
Pass outlineRef when using <Sequence layout="none"> so the Studio can draw an outline around the rendered element.
Use a stable componentIdentity so saved Studio edits can be associated with the component.
import React, {forwardRef, useImperativeHandle, useRef} from 'react';
import {
Interactive,
Sequence,
type InteractiveBaseProps,
type InteractiveTransformProps,
type InteractivitySchema,
type SequenceControls,
} from 'remotion';
type BadgeProps = InteractiveBaseProps &
InteractiveTransformProps & {
readonly children?: React.ReactNode;
readonly color?: string;
readonly padding?: number;
};
const badgeSchema = {
...Interactive.baseSchema,
color: {
type: 'color',
default: '#0b84ff',
description: 'Color',
},
padding: {
type: 'number',
min: 0,
step: 1,
default: 16,
description: 'Padding',
hiddenFromList: false,
},
...Interactive.transformSchema,
} as const satisfies InteractivitySchema;
const BadgeInner = forwardRef<
HTMLDivElement,
BadgeProps & {
readonly controls: SequenceControls | undefined;
}
>(
(
{
children,
color = '#0b84ff',
padding = 16,
style,
name,
controls,
...sequenceProps
},
ref,
) => {
const outlineRef = useRef<HTMLDivElement>(null);
useImperativeHandle(ref, () => outlineRef.current as HTMLDivElement, []);
return (
<Sequence
layout="none"
{...sequenceProps}
name={name ?? '<Badge>'}
controls={controls}
outlineRef={outlineRef}
>
<div
ref={outlineRef}
style={{
...style,
display: 'inline-flex',
borderRadius: 999,
backgroundColor: color,
color: 'white',
fontWeight: 700,
padding,
}}
>
{children}
</div>
</Sequence>
);
},
);
// ---cut---
export const Badge = Interactive.withSchema({
Component: BadgeInner,
componentName: '<Badge>',
componentIdentity: 'com.example.Badge',
schema: badgeSchema,
supportsEffects: false,
});
import {AbsoluteFill} from 'remotion';
import {Badge} from './Badge';
export const MyComp = () => {
return (
<AbsoluteFill>
<Badge
from={30}
durationInFrames={90}
color="#ff5c7a"
style={{translate: '120px 80px'}}
>
Sale
</Badge>
</AbsoluteFill>
);
};
The component now has a timeline row, inherits the common <Sequence> props and exposes color, padding and transform controls in the Studio.
Interactive.baseSchemaUse it when the component renders a <Sequence> internally.
Interactive.transformSchemaUse it when the component accepts a style prop and applies it to the rendered element.
Interactive.premountSchemaUse it when the component forwards premountFor, postmountFor, styleWhilePremounted and styleWhilePostmounted to a <Sequence> with layout="absolute-fill".