Skip to content
Esc
navigateopen⌘Jpreview
On this page

Video components

Install polished, temporal React components as source and compose them directly inside video.tsx.

Odori’s component registry follows the shadcn model: installation copies source into your project instead of hiding implementation behind a package.

pnpm odori add \
  @odori/stage \
  @odori/title-reveal \
  @odori/code-proof \
  @odori/terminal \
  @odori/browser-demo \
  @odori/connection-story \
  @odori/end-card

pnpm odori registry prints the catalog with each component’s aspect ratios, minimum duration, and reduced-motion behavior. Registry dependencies install with the component that needs them.

Every component also has a page in the component catalog, one per registry entry, where the real component plays in the browser at any frame and format, next to its timing contract, props, and the exact source odori add copies.

  • videos/
    • components/
      • stage/
        • stage.tsx
        • stage.preview.tsx
      • title-reveal/
        • title-reveal.tsx
        • title-reveal.preview.tsx
      • code-proof/
        • code-proof.tsx
        • code-proof.preview.tsx
      • terminal/
        • terminal.tsx
        • terminal.preview.tsx
      • end-card/
        • end-card.tsx
        • end-card.preview.tsx

Use the components as ordinary React:

<Scene id="proof" duration="8s">
  <Stage grid={false}>
    <CodeProof
      code={'export const preview = "instant";'}
      focus={[1]}
      language="tsx"
      title="videos/launch/video.tsx"
    />
  </Stage>
</Scene>

Temporal contracts

Video components need more than prop types. Registry metadata describes:

  • supported aspect ratios
  • recommended and minimum duration
  • natural entrance and exit frames
  • content limits, such as maximum headline length
  • reduced-motion behavior
  • required fonts and audio

odori test and the registry test suite check these contracts, while the copied implementation stays fully editable.

Scale with the frame, not the width

Components multiply their design values by useDesignScale(), which measures the shorter side of the frame against a 1080px reference. The same component reads correctly at 1920x1080, 1080x1920, and 1080x1080 without a separate vertical variant.

const scale = useDesignScale();
<div style={{fontSize: 116 * scale, letterSpacing: "-0.045em"}}>{title}</div>;

Component development in Studio

The component source stays independent from its development fixture. A sibling *.preview.tsx file defines representative props, controls, duration, canvas size, and edge cases for Studio. The fixture is similar to a Storybook story with a deterministic frame clock.

Reuse application components

Videos can import deterministic components from an application’s root components/ directory. Keep those components under their existing product ownership instead of duplicating them for video.

Add an adapter under videos/components/product/ when the application component expects routing, live data, or browser interaction. The adapter supplies frozen data and disables behavior that depends on wall-clock or application state.

Initial component library

Family Components
Typography Title reveal, metric callout, captions
Developer proof Terminal, code proof
Product UI Browser demo
Narrative Connection story
Brand Stage, end card

Install the agent quickstart

The repository also ships an agent skill that turns project evidence into a coherent first cut and installs a small, story-specific component set:

npx skills add allenzhou101/odori \
  --skill odori-quickstart \
  --yes

Source ownership

Registry provenance is recorded in .odori/components.json, so the CLI can tell a pristine component from one you have edited and from one the registry has moved past.

odori diff                 # what changed, in your project and upstream
odori diff terminal --full # the diff itself
odori update               # apply upstream changes to untouched components

Updates are opt-in and source-aware. A component that is both edited locally and changed upstream is reported as diverged and left alone until you pass --force.

Was this page helpful?