Focus in
Words settle from a soft blur into a crisp line.
Customize
import {Scene, Video} from "odori";
import {FocusIn} from "../components/focus-in/focus-in";
<Video>
<Scene id="focus-in" duration="4s">
<FocusIn
text="Clarity takes a moment"
detail="Then it holds."
stagger={5}
blur={22}
/>
</Scene>
</Video>Installation
pnpm odori add component focus-innpx odori add component focus-inbunx odori add component focus-inEdit the source in videos/components/focus-in/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/focus-in/focus-in.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type FocusInProps = {
text: string;
detail?: string;
/** Frames between one word and the next. */
stagger?: number;
/** Starting blur radius in design pixels. */
blur?: number;
};
/**
* Each word starts blurred, slightly oversized and transparent, then settles
* crisp in place. The size change is a transform rather than a font-size
* change, so the line is laid out once and never reflows while a word is
* still soft.
*/
export const FocusIn = ({text, detail, stagger = 5, blur = 22}: FocusInProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const words = text.split(/\s+/).filter(Boolean);
const settled = 6 + words.length * stagger + 26;
return (
<Fill
style={{
alignItems: "center",
gap: 32 * scale,
justifyContent: "center",
padding: `${120 * scale}px ${140 * scale}px`,
}}
>
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: `0 ${30 * scale}px`,
justifyContent: "center",
maxWidth: 1560 * scale,
}}
>
{words.map((word, index) => {
const delay = 6 + index * stagger;
const progress = interpolate(frame, [delay, delay + 26], [0, 1], {easing: Easing.standard});
return (
<span
key={`${word}-${index}`}
style={{
display: "inline-block",
filter: `blur(${(1 - progress) * blur * scale}px)`,
fontSize: 120 * scale,
fontWeight: 580,
letterSpacing: "-0.04em",
lineHeight: 1.06,
opacity: progress,
transform: `scale(${1 + (1 - progress) * 0.08})`,
}}
>
{word}
</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/focus-in/focus-in.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {FocusIn} from "./focus-in";
export default defineComponentPreview({
title: "Focus in",
category: "Typography/Titles",
description: "Words settle from a soft blur into a crisp line.",
component: FocusIn,
canvas: {width: 1920, height: 1080, duration: "4s"},
controls: {
text: {type: "text", defaultValue: "Clarity takes a moment", maxLength: 48},
detail: {type: "text", defaultValue: "Then it holds."},
stagger: {type: "number", defaultValue: 5, min: 1, max: 12},
blur: {type: "number", defaultValue: 22, min: 4, max: 60},
},
examples: [
{name: "Default", props: {text: "Clarity takes a moment", detail: "Then it holds."}},
{name: "Heavy blur", props: {text: "Out of the fog", blur: 48, stagger: 8}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {FocusIn} from "../components/focus-in/focus-in";
<Video>
<Scene id="focus-in" duration="4s">
<FocusIn
text="Clarity takes a moment"
detail="Then it holds."
stagger={5}
blur={22}
/>
</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
- 70 frames · 2.33s
- Entrance
- 62 frames
- Exit
- 0 frames
- Reduced motion
- words fade in without blur or scale
- Requires
- sans font
- Content limits
- text ≤ 48
Props
| Prop | Type | Default | Required |
|---|---|---|---|
text | string | — | Yes |
detail | string | — | — |
staggerFrames between one word and the next. | number | 5 | — |
blurStarting blur radius in design pixels. | number | 22 | — |
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.Glitch textA title that arrives through brief seeded interference bursts.Kinetic centerA centered statement assembled from timed word groups.