reelcn

Storyboard

Write a video as JSON (theme, brand, audio and a list of scenes), and Storyboard renders it and works out its length.

Register it once

npx shadcn add @reelcn/storyboard
// src/Root.tsx
import { Composition } from "remotion";
import { StoryVideo, storyMetadata, storySchema } from "./reelcn/storyboard";

export const Root = () => (
  <Composition
    id="Storyboard"
    component={StoryVideo}
    schema={storySchema}
    calculateMetadata={storyMetadata}
    defaultProps={{ scenes: [{ type: "title", title: "Hello from a story" }] }}
    width={1920}
    height={1080}
    fps={30}
    durationInFrames={1}
  />
);

Render a story

{
  "theme": "neon",
  "scenes": [
    { "type": "title", "title": "Rendered from JSON", "subtitle": "npx remotion render Storyboard" },
    { "type": "chart", "kind": "bar", "title": "Weekly renders",
      "data": [{ "label": "Mon", "value": 12 }, { "label": "Tue", "value": 30 }, { "label": "Wed", "value": 22 }] },
    { "type": "cta", "title": "Your turn", "button": "Write a story" }
  ]
}
npx remotion render Storyboard out.mp4 --props=story.json

That story runs 294 frames: 114 + 120 + 90 frames of scenes, minus two 15-frame fades.

The story

FieldWhat it does
themeA theme preset name. Default: the surrounding theme, or midnight.
brand{ accent?, logo?, font? }, laid over the theme.
fpsFrames per second. Default 30.
audio{ music?, musicVolume?, voiceover?, captions? }. captions is a caption array or the URL of a JSON file holding one.
defaults{ transition?, transitionFrames?, sfx? } for every scene. Default: a 15-frame fade.
scenesAt least one scene.

Scene types

Every scene also takes these fields:

  • duration: seconds.
  • transition: into the next scene, or "none" for a hard cut.
  • sfx: a sound effect name, or false.
  • background: a background name.
TypeFieldsNatural length
titletitle, subtitle?, kicker?reading time
texttext, accent? (words to color)reading time
bulletstitle?, items[{ text, detail?, badge? }]reading time
imagesrc, caption?, zoom?3 s
videosrc, trimStart?, trimEnd?, muted?5 s, or trimEnd − trimStart
devicedevice: phone | laptop | browser, src?, url?3 s, or 5 s for a video
codecode, language?, title?, highlight?typing at 40 characters a second, + 1.5 s
terminallines[{ type: command | output, text }], title?typing time + 1.5 s
chartkind: bar | line | donut, data[{ label, value }], title?4 s
statlabel, value, prefix?, suffix?, delta?, caption?4 s
quotequote, name, role?, avatar?reading time
postname, text, handle?, avatar?, metrics?reading time
ctatitle, button?, url?reading time
logotext, src? (defaults to brand.logo)reading time
splitleft, right (any scene but split), labels?the longer side

Reading time is 2.5 words a second plus one second, and never under two seconds. A scene's duration always wins. The story's length is the sum of its scenes minus the frames its transitions overlap.

Names

  • Transitions: fade, brand-sweep, card-push, circle-burst, glitch, light-flash, shutter, slice-slide, split-doors, stripe-wipe, tile-reveal, none.
  • Backgrounds: aurora, beams, bokeh, brand-solid, dots, grain, gradient-mesh, grid, spotlight, starfield.
  • Sound effects: see Audio and SFX.

Errors

A broken story fails before rendering, and the error names the scene and field:

Error: Invalid story:
✖ Invalid input: expected string, received undefined
  → at scenes[0].title
✖ unknown scene type "hero"; expected one of title, text, bullets, image, video, device, code, terminal, chart, stat, quote, post, cta, logo, split
  → at scenes[1].type

Custom scenes

import { z } from "zod";
import { defineScene, Storyboard } from "./reelcn/storyboard";

const mapScene = defineScene({
  type: "map",
  schema: z.object({ city: z.string() }),
  component: ({ city }) => <h1>{city}</h1>,
  duration: () => 3,
});

<Storyboard story={story} scenes={[mapScene]} />;

On this page