Character rise
Characters rise with a crisp per-letter stagger.
Customize
import {Scene, Video} from "odori";
import {CharacterRise} from "../components/character-rise/character-rise";
<Video>
<Scene id="character-rise" duration="4s">
<CharacterRise
text="Momentum"
detail="Motion with a purpose."
stagger={2}
/>
</Scene>
</Video>Installation
pnpm odori add component character-risenpx odori add component character-risebunx odori add component character-riseEdit the source in videos/components/character-rise/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/character-rise/character-rise.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type CharacterRiseProps = {
text: string;
detail?: string;
/** Frames between one character and the next. */
stagger?: number;
};
/**
* Characters rise out of a clipped baseline one after another. Spaces hold
* their slot so the line never reflows while it assembles.
*/
export const CharacterRise = ({text, detail, stagger = 2}: CharacterRiseProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const characters = [...text];
const settled = 8 + characters.length * stagger + 22;
return (
<Fill
style={{
alignItems: "center",
gap: 30 * scale,
justifyContent: "center",
padding: `${120 * scale}px ${140 * scale}px`,
}}
>
<div style={{display: "flex", overflow: "hidden", paddingBottom: 14 * scale}}>
{characters.map((character, index) => {
const delay = 8 + index * stagger;
const progress = interpolate(frame, [delay, delay + 22], [0, 1], {easing: Easing.standard});
return (
<span
key={`${character}-${index}`}
style={{
display: "inline-block",
fontSize: 132 * scale,
fontWeight: 600,
letterSpacing: "-0.04em",
lineHeight: 1.02,
opacity: progress,
transform: `translateY(${(1 - progress) * 84 * scale}px)`,
whiteSpace: "pre",
}}
>
{character}
</span>
);
})}
</div>
{detail ? (
<div
style={{
color: brand.colors.muted,
fontSize: 34 * scale,
opacity: interpolate(frame, [settled, settled + 18], [0, 1], {easing: Easing.standard}),
}}
>
{detail}
</div>
) : null}
</Fill>
);
};
videos/components/character-rise/character-rise.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {CharacterRise} from "./character-rise";
export default defineComponentPreview({
title: "Character rise",
category: "Typography/Titles",
description: "Characters rise with a crisp per-letter stagger.",
component: CharacterRise,
canvas: {width: 1920, height: 1080, duration: "4s"},
controls: {
text: {type: "text", defaultValue: "Momentum", maxLength: 22},
detail: {type: "text", defaultValue: "Motion with a purpose."},
stagger: {type: "number", defaultValue: 2, min: 1, max: 8},
},
examples: [
{name: "Default", props: {text: "Momentum", detail: "Motion with a purpose."}},
{name: "Slow", props: {text: "Deliberate", stagger: 5}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {CharacterRise} from "../components/character-rise/character-rise";
<Video>
<Scene id="character-rise" duration="4s">
<CharacterRise
text="Momentum"
detail="Motion with a purpose."
stagger={2}
/>
</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
- 34 frames
- Exit
- 10 frames
- Reduced motion
- the word fades without per-letter motion
- Requires
- sans font
- Content limits
- text ≤ 22
Props
| Prop | Type | Default | Required |
|---|---|---|---|
text | string | — | Yes |
detail | string | — | — |
staggerFrames between one character and the next. | number | 2 | — |
Related components
All componentsCaptionsFrame-accurate caption cues for sound-off playback.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.Kinetic centerA centered statement assembled from timed word groups.