Word cascade
Words enter in a paced vertical sequence.
Customize
import {Scene, Video} from "odori";
import {WordCascade} from "../components/word-cascade/word-cascade";
<Video>
<Scene id="word-cascade" duration="4s">
<WordCascade
words={[
"Write.",
"Preview.",
"Ship."
]}
detail="One workflow, every launch."
stagger={12}
/>
</Scene>
</Video>Installation
pnpm odori add component word-cascadenpx odori add component word-cascadebunx odori add component word-cascadeEdit the source in videos/components/word-cascade/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/word-cascade/word-cascade.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type WordCascadeProps = {
/** One word per line, revealed in order. */
words: string[];
detail?: string;
/** Frames between one word and the next. */
stagger?: number;
};
/**
* Words arrive down a vertical column, each one lifting into place while the
* one above settles. The cascade is derived from the frame, so the rhythm is
* identical in preview and in the render.
*/
export const WordCascade = ({words, detail, stagger}: WordCascadeProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const step = stagger ?? brand.motion.staggerFrames * 4;
return (
<Fill
style={{
alignItems: "flex-start",
gap: 18 * scale,
justifyContent: "center",
padding: `${120 * scale}px ${150 * scale}px`,
}}
>
{words.map((word, index) => {
const delay = 6 + index * step;
const progress = interpolate(frame, [delay, delay + 26], [0, 1], {easing: Easing.standard});
return (
<div key={`${word}-${index}`} style={{overflow: "hidden", paddingBottom: 10 * scale}}>
<span
style={{
display: "inline-block",
fontSize: 128 * scale,
fontWeight: 570,
letterSpacing: "-0.05em",
lineHeight: 1,
opacity: progress,
transform: `translateY(${(1 - progress) * 96 * scale}px)`,
}}
>
{word}
</span>
</div>
);
})}
{detail ? (
<div
style={{
color: brand.colors.muted,
fontSize: 34 * scale,
marginTop: 14 * scale,
opacity: interpolate(frame, [6 + words.length * step, 6 + words.length * step + 20], [0, 1], {
easing: Easing.standard,
}),
}}
>
{detail}
</div>
) : null}
</Fill>
);
};
videos/components/word-cascade/word-cascade.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {WordCascade} from "./word-cascade";
export default defineComponentPreview({
title: "Word cascade",
category: "Typography/Titles",
description: "Words enter in a paced vertical sequence.",
component: WordCascade,
canvas: {width: 1920, height: 1080, duration: "4s"},
controls: {
detail: {type: "text", defaultValue: "One workflow, every launch."},
stagger: {type: "number", defaultValue: 12, min: 4, max: 30},
},
examples: [
{name: "Default", props: {words: ["Write.", "Preview.", "Ship."], detail: "One workflow, every launch."}},
{name: "Two beats", props: {words: ["Author.", "Render."], stagger: 18}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {WordCascade} from "../components/word-cascade/word-cascade";
<Video>
<Scene id="word-cascade" duration="4s">
<WordCascade
words={[
"Write.",
"Preview.",
"Ship."
]}
detail="One workflow, every launch."
stagger={12}
/>
</Scene>
</Video>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Typography
- Recommended duration
- 120 frames · 4s
- Minimum duration
- 48 frames · 1.6s
- Entrance
- 30 frames
- Exit
- 10 frames
- Reduced motion
- all words fade in together
- Requires
- sans font
- Content limits
- words ≤ 5, wordLength ≤ 18
Props
| Prop | Type | Default | Required |
|---|---|---|---|
wordsOne word per line, revealed in order. | string[] | — | Yes |
detail | string | — | — |
staggerFrames between one word and the next. | number | — | — |
Related components
All componentsCaptionsFrame-accurate caption cues for sound-off playback.Character riseCharacters rise with a crisp per-letter stagger.Draw titleAn outline draws across the line, then fills solid.Flap boardA split-flap board clatters through to the target string.Focus inWords settle from a soft blur into a crisp line.Glitch textA title that arrives through brief seeded interference bursts.