Back to Remotion

Make a component interactive

packages/docs/docs/studio/make-component-interactive.mdx

4.0.4837.1 KB
Original Source

Make a component interactive<AvailableFrom v="4.0.479" />

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().

Create a schema

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.

tsx
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;

Forward controls

The inner component receives a controls prop from Interactive.withSchema().

Forward it to the <Sequence> that represents your component in the timeline.

tsx
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.

Wrap the component

Use a stable componentIdentity so saved Studio edits can be associated with the component.

tsx
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,
});

Use the component

tsx
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.

Choosing schema fragments

Interactive.baseSchema

Use it when the component renders a <Sequence> internally.

Interactive.transformSchema

Use it when the component accepts a style prop and applies it to the rendered element.

Interactive.premountSchema

Use it when the component forwards premountFor, postmountFor, styleWhilePremounted and styleWhilePostmounted to a <Sequence> with layout="absolute-fill".

See also