Can we make the interactivity of a component use less boilerplate?
This is what ChatGPT says:
Yes. The current API leaks almost every internal registration detail into user code. A simple component must coordinate the props type, schema fragments, schema defaults, controls, <Sequence>, three ref hooks/props, component naming, and supportsEffects.
There are two strong precedents already in the repo:
Interactive.Divcentralizes this inmakeInteractiveElement().@remotion/shapescentralizes it inRenderSvg, which is why components such asRectstay concise.- Complex components such as
Gif,Video, and Rive genuinely need the current low-level control.
I would add a high-level API for the common “one timeline item, one outline element” case:
type BadgeProps = {
readonly children?: React.ReactNode;
};
export const Badge = Interactive.createComponent<BadgeProps>({
name: 'Badge',
fields: {
color: Interactive.color('#0b84ff'),
padding: Interactive.number(16, {min: 0, step: 1}),
},
styleControls: ['transform'],
render: ({children, color, padding, style}, ref) => (
<div
ref={ref}
style={{
...style,
display: 'inline-flex',
borderRadius: 999,
backgroundColor: color,
padding,
}}
>
{children}
</div>
),
});
createComponent() could:
- Include
baseSchemaautomatically and addInteractiveBasePropsto the returned component. - Build schema fields through typed factories, defaulting
hiddenFromListand descriptions sensibly. - Use schema defaults as runtime defaults, eliminating duplicated
color = …anddefault: …. - Own
<Sequence layout="none">,controls,outlineRef, and forwarded-ref merging. - Derive
<Badge>, the default timeline name, anddisplayNamefromname. - Default
supportsEffectstofalse.
For more complicated components, I would add a lower-level primitive underneath it:
const BadgeInner = ({children, style, ...timelineProps}: BadgeProps) => (
<Interactive.Root name="Badge" {...timelineProps} asChild>
<div style={style}>{children}</div>
</Interactive.Root>
);
Interactive.withSchema() would provide controls through context, while Interactive.Root would consume them, register the sequence, and attach/merge a ref onto its single child. I would not make every ordinary <Sequence> implicitly consume those controls: components with nested or multiple sequences would become ambiguous.
The highest-value sequence is therefore:
- Introduce
Interactive.Rootas the explicit registration boundary. - Build
Interactive.createComponent()on top for the dominant simple case. - Add typed field factories such as
number(),color(),boolean(), andenum(). - Keep
withSchema()as the advanced escape hatch.
That would turn the documented Badge from roughly 70 lines of integration code into about 20–25, while preserving a path for media, effects, premounting, multiple roots, and custom timeline behavior.
Source: remotion-dev/remotion