Title reveal
A staggered, word-masked headline.
Customize
import {Scene, Video} from "odori";
import {TitleReveal} from "../components/title-reveal/title-reveal";
<Video>
<Scene id="title-reveal" duration="4s">
<TitleReveal
title="Ship the story."
detail="An independent React video framework."
align="center"
/>
</Scene>
</Video>Installation
pnpm odori add component title-revealnpx odori add component title-revealbunx odori add component title-revealEdit the source in videos/components/title-reveal/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/title-reveal/title-reveal.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type TitleRevealProps = {
title: string;
detail?: string;
align?: "left" | "center";
};
/**
* A masked, word-staggered headline. Each word rises out of a clipped line
* box, so the reveal reads as typography rather than a fade.
*/
export const TitleReveal = ({title, detail, align = "center"}: TitleRevealProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const lines = title.split("\n");
let wordIndex = 0;
return (
<Fill
style={{
alignItems: align === "center" ? "center" : "flex-start",
gap: 34 * scale,
justifyContent: "center",
padding: `${120 * scale}px ${140 * scale}px`,
textAlign: align,
}}
>
<div style={{display: "grid", gap: 6 * scale, maxWidth: 1560 * scale}}>
{lines.map((line, lineNumber) => (
<div
key={`${line}-${lineNumber}`}
style={{
display: "flex",
flexWrap: "wrap",
gap: `0 ${26 * scale}px`,
justifyContent: align === "center" ? "center" : "flex-start",
overflow: "hidden",
paddingBottom: 12 * scale,
}}
>
{line.split(/\s+/).map((word) => {
const delay = 4 + wordIndex * brand.motion.staggerFrames;
wordIndex += 1;
const progress = interpolate(frame, [delay, delay + 24], [0, 1], {easing: Easing.standard});
return (
<span
key={`${word}-${wordIndex}`}
style={{
display: "inline-block",
fontSize: 116 * scale,
fontWeight: 560,
letterSpacing: "-0.045em",
lineHeight: 1.04,
opacity: progress,
transform: `translateY(${(1 - progress) * 78 * scale}px)`,
}}
>
{word}
</span>
);
})}
</div>
))}
</div>
{detail ? (
<div
style={{
color: brand.colors.muted,
fontSize: 36 * scale,
letterSpacing: "-0.015em",
maxWidth: 1100 * scale,
opacity: interpolate(frame, [16, 34], [0, 1], {easing: Easing.standard}),
transform: `translateY(${interpolate(frame, [16, 34], [14 * scale, 0], {easing: Easing.standard})}px)`,
}}
>
{detail}
</div>
) : null}
</Fill>
);
};
videos/components/title-reveal/title-reveal.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {TitleReveal} from "./title-reveal";
export default defineComponentPreview({
title: "Title reveal",
category: "Typography/Titles",
description: "A staggered, word-masked headline.",
component: TitleReveal,
canvas: {width: 1920, height: 1080, duration: "4s"},
controls: {
title: {type: "text", defaultValue: "Ship the story.", maxLength: 64},
detail: {type: "text", defaultValue: "An independent React video framework."},
align: {type: "select", defaultValue: "center", options: ["left", "center"]},
},
examples: [
{name: "Default", props: {title: "Ship the story."}},
{name: "Two lines", props: {title: "Build videos\nlike applications.", detail: "Author. Preview. Ship."}},
{name: "Long copy", props: {title: "Build videos like applications, not like exports.", align: "left"}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {TitleReveal} from "../components/title-reveal/title-reveal";
<Video>
<Scene id="title-reveal" duration="4s">
<TitleReveal
title="Ship the story."
detail="An independent React video framework."
align="center"
/>
</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
- 45 frames · 1.5s
- Entrance
- 20 frames
- Exit
- 10 frames
- Reduced motion
- fades without translation
- Requires
- sans font
- Content limits
- title ≤ 64
Props
| Prop | Type | Default | Required |
|---|---|---|---|
title | string | — | Yes |
detail | string | — | — |
align | "left" | "center" | "center" | — |
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.