#11427·remotion

Can we make the interactivity of a component use less boilerplate?

Author: JonnyBurgerCreated Sep 17, 2026Updated Sep 17, 2026

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.Div centralizes this in makeInteractiveElement().
  • @remotion/shapes centralizes it in RenderSvg, which is why components such as Rect stay 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 baseSchema automatically and add InteractiveBaseProps to the returned component.
  • Build schema fields through typed factories, defaulting hiddenFromList and descriptions sensibly.
  • Use schema defaults as runtime defaults, eliminating duplicated color = … and default: ….
  • Own <Sequence layout="none">, controls, outlineRef, and forwarded-ref merging.
  • Derive <Badge>, the default timeline name, and displayName from name.
  • Default supportsEffects to false.

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:

  1. Introduce Interactive.Root as the explicit registration boundary.
  2. Build Interactive.createComponent() on top for the dominant simple case.
  3. Add typed field factories such as number(), color(), boolean(), and enum().
  4. 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.