packages/docs/docs/studio/interactivity-best-practices.mdx
Write editable Remotion components so the Studio interactivity can understand the values that should be changed visually.
When a value is shown as computed in the Studio, it often means that the value is hidden behind a variable, helper function, conditional expression or transform string.
When converting a component for Studio interactivity, apply all of these rules together:
Interactive.* for editable HTML or SVG elements, or Interactive.withSchema() for custom components.name prop to the editable element or custom component.interpolate() calls.interpolate() input and output ranges as hardcoded arrays, such as [0, 30].translate, scale, rotate, transformOrigin and opacity instead of hiding values in a transform string.translate for animated movement and canvas dragging. Keep top, left, right and bottom for static anchoring.<Composition> metadata and defaultProps inline unless the value is truly dynamic.Interactive.* for tweakable elements {#use-interactive-for-tweakable-elements}Use the Interactive namespace when an HTML or SVG element should be selected, moved or edited in the Studio.
// 👍 Selectable and editable in the Studio
<Interactive.Div name="Greeting card" style={{fontSize: 80, padding: 24}}>
Hello
</Interactive.Div>
// 👎 A regular element is not Studio-editable
<div style={{fontSize: 80, padding: 24}}>Hello</div>
For a custom component, see Make a component interactive.
Add a name prop to interactive elements and custom interactive components.
Use a short label that describes the element in the scene.
The name is shown in the Studio timeline and helps agents identify the correct element when making visual edits.
// 👍 Easy to find in the Studio timeline and by agents
<Interactive.Div name="Hero title" style={{fontSize: 80}}>
Launch day
</Interactive.Div>
// 👎 Omitted names are hard to identify in the Studio timeline and by agents
<Interactive.Div style={{fontSize: 80}}>
Launch day
</Interactive.Div>
Put values that should be edited in the Studio directly into the JSX. This applies to static style values, keyframes, easing and transform props.
Write animations as inline interpolate() calls on the property that changes.
Do not calculate the value in a local variable and pass the variable into style.
Do not animate with frame math such as frame * 2, modulo expressions, helper functions or transform strings.
Keep the input range and output range directly in the interpolate() call.
The input range must use hardcoded numbers, not variables or expressions such as [sendStartFrame, sendStartFrame + duration].
// 👍 Inline values can be standardized and keyframed
<Interactive.Div
name="Product card"
style={{
color: 'white',
fontSize: 80,
scale: interpolate(frame, [0, 30], [0, 1], {
easing: Easing.spring({damping: 200}),
}),
rotate: interpolate(frame, [0, 30], ['0deg', '20deg']),
translate: interpolate(frame, [0, 30], ['0px 0px', '0px 120px']),
}}
/>
// 👎 Values hidden behind variables, helpers or strings become computed
const container: React.CSSProperties = {
color: 'white',
fontSize: 80,
};
const translateY = interpolate(frame, [0, 30], [0, 120]);
const rotation = interpolate(frame, [0, 30], [0, 20]);
<Interactive.Div
name="Product card"
style={{
...container,
transform: `translateY(${translateY}px) rotate(${rotation}deg)`,
}}
/>
Use standalone spring() only when the animation needs a frame-driven physical spring simulation.
Use individual CSS transform properties such as scale, rotate and translate instead of composing a transform string.
Pass easing to the same interpolate() call instead of nesting another interpolate() call to create an eased progress value.
// 👍 Motion, opacity and scale are visible to the Studio
<Interactive.Div
name="Prompt bubble"
style={{
position: 'absolute',
right: 56,
top: 96,
opacity: interpolate(frame, [30, 50], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
}),
translate: interpolate(frame, [30, 50], ['0px 32px', '0px 0px'], {
easing: Easing.out(Easing.cubic),
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
}),
scale: interpolate(frame, [30, 50], [0.92, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp',
}),
transformOrigin: '100% 100%',
}}
/>
// 👎 The Studio sees computed values instead of editable props
const sendStartFrame = 30;
const promptTravelDurationInFrames = 20;
const promptBubbleTop = interpolate(frame, [30, 50], [128, 96]);
const promptBubbleOpacity = interpolate(frame, [30, 50], [0, 1]);
const easedProgress = Easing.out(Easing.cubic)(
interpolate(
frame,
[sendStartFrame, sendStartFrame + promptTravelDurationInFrames],
[0, 1],
),
);
<Interactive.Div
name="Prompt bubble"
style={{
position: 'absolute',
right: 56,
top: promptBubbleTop,
opacity: promptBubbleOpacity,
translate: interpolate(easedProgress, [0, 1], ['0px 32px', '0px 0px']),
transformOrigin: '100% 100%',
}}
/>
Use top, left, right and bottom for static layout anchors.
When the value changes over time or should be draggable/keyframable in the Studio, put the moving part into translate.
Make this interactive in the Remotion Studio: https://www.remotion.dev/docs/studio/interactivity-best-practices.md
When scaffolding a composition, keep the component and its <Composition> registration in the same file.
Keep static width, height, fps, durationInFrames and defaultProps inline.
The Props editor can save visual edits back to your code when defaultProps is an inline object literal on <Composition> or <Still>.
If the Studio shows `defaultProps` prop must be a hardcoded value in the <Composition/> tag, move the values into the JSX.
// 👍 Static metadata and defaults are inline and nearby
<Composition
id="my-video"
component={MyComponent}
durationInFrames={150}
fps={30}
width={1920}
height={1080}
defaultProps={{title: 'Hello', color: '#0b84ff'}}
/>
// 👎 Static metadata is hidden behind helpers or variables
const defaultProps = {title: 'Hello', color: '#0b84ff'};
const calculateMetadata = () => {
return {durationInFrames: 150, fps: 30, width: 1920, height: 1080};
};
<Composition
id="my-video"
component={MyComponent}
calculateMetadata={calculateMetadata}
defaultProps={{
title: 'Hello',
}}
/>
Use calculateMetadata() when metadata depends on dynamic input props, fetched data or asset metadata.
Make this interactive in the Remotion Studio: https://www.remotion.dev/docs/studio/interactivity-best-practices.md
Keep editable effect parameters inline in the effect call and keep the effect array shape unconditional.
If an effect parameter is animated, write it as an inline interpolate() call in the effect call.
// 👍 Parameters are inline and the array shape is stable
<CanvasImage
src={src}
width={1280}
height={720}
effects={[
radialProgressiveBlur({
center: [0.5, 0.5],
width: 1.2,
height: 0.8,
start: 0.2,
rotation: interpolate(frame, [0, 120], [0, 180]),
}),
]}
/>
// 👎 Values are hidden and the effect only exists conditionally
const center = [0.5, 0.5] as const;
const rotation = frame * 1.5;
<CanvasImage
src={src}
width={1280}
height={720}
effects={enabled ? [
radialProgressiveBlur({
center,
rotation,
}),
] : []}
/>
Render separate elements if one version should have effects and another should not.
Make this interactive in the Remotion Studio: https://www.remotion.dev/docs/studio/interactivity-best-practices.md