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().
For code that remains editable after visual changes, follow Interactivity best practices.
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.
Use Interactive.cropSchema and InteractiveCropProps to opt a component into the crop controls inherited from an absolute-fill <Sequence>.
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;
Add a captions field when a custom component accepts Caption[] data:
import {Interactive, type InteractivitySchema} from 'remotion';
const captionsSchema = {
...Interactive.baseSchema,
...Interactive.captionsSchema,
} as const satisfies InteractivitySchema;
Studio can edit caption text when the prop is a direct JSX array literal, such as captions={[...]}. Variables, imports, computed expressions, and satisfies expressions are read-only.
controlsThe inner component receives a controls prop from Interactive.withSchema().
Forward it unchanged to the <Sequence> that represents your component in the timeline. Forwarding it also allows the Studio to associate the timeline item with the outer component's source location.
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.
Set componentIdentity to null for a component defined in your project.
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: null,
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.cropSchema<AvailableFrom v="4.0.500" />Use it with InteractiveCropProps when the component applies crop props to its rendered element or forwards them to an absolute-fill <Sequence>.
Interactive.premountSchemaUse it when the component forwards premountFor and postmountFor to a <Sequence>.