Timeline
Milestones revealed along a timeline.
Customize
import {Scene, Video} from "odori";
import {Timeline} from "../components/timeline/timeline";
<Video>
<Scene id="timeline" duration="5s">
<Timeline
milestones={[
{
label: "Idea",
detail: "one sentence"
},
{
label: "Draft",
detail: "scenes in place"
},
{
label: "Review",
detail: "contract tests"
},
{
label: "Launch",
detail: "one export"
}
]}
headline="From idea to launch"
/>
</Scene>
</Video>Installation
pnpm odori add component timelinenpx odori add component timelinebunx odori add component timelineEdit the source in videos/components/timeline/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/timeline/timeline.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type Milestone = {label: string; detail?: string};
export type TimelineProps = {
milestones: Milestone[];
headline?: string;
};
/**
* Milestones along one measured axis. The axis draws first and the markers
* land on it in order, so the spacing reads as time rather than as a list.
*/
export const Timeline = ({milestones, headline}: TimelineProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const axis = interpolate(frame, [8, 44], [0, 1], {easing: Easing.standard});
return (
<Fill style={{gap: 60 * scale, justifyContent: "center", padding: `${110 * scale}px ${120 * scale}px`}}>
{headline ? (
<div style={{fontSize: 58 * scale, fontWeight: 560, letterSpacing: "-0.04em", opacity: interpolate(frame, [0, 16], [0, 1])}}>
{headline}
</div>
) : null}
<div style={{position: "relative"}}>
<div
style={{
background: brand.colors.border,
height: 2 * scale,
transform: `scaleX(${axis})`,
transformOrigin: "left",
width: "100%",
}}
/>
<div style={{display: "flex", justifyContent: "space-between", marginTop: -12 * scale}}>
{milestones.map((milestone, index) => {
const at = 24 + index * 12;
const enter = interpolate(frame, [at, at + 18], [0, 1], {easing: Easing.standard});
return (
<div key={milestone.label} style={{alignItems: "center", display: "grid", gap: 16 * scale, justifyItems: "center", opacity: enter}}>
<span
style={{
background: index === milestones.length - 1 ? brand.colors.accent : brand.colors.foreground,
borderRadius: 999,
height: 22 * scale,
transform: `scale(${0.6 + enter * 0.4})`,
width: 22 * scale,
}}
/>
<span style={{fontSize: 34 * scale, fontWeight: 540}}>{milestone.label}</span>
{milestone.detail ? (
<span style={{color: brand.colors.muted, fontSize: 26 * scale}}>{milestone.detail}</span>
) : null}
</div>
);
})}
</div>
</div>
</Fill>
);
};
videos/components/timeline/timeline.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {Timeline} from "./timeline";
export default defineComponentPreview({
title: "Timeline",
category: "Narrative",
description: "Milestones revealed along a timeline.",
component: Timeline,
canvas: {width: 1920, height: 1080, duration: "5s"},
controls: {headline: {type: "text", defaultValue: "From idea to launch"}},
examples: [
{
name: "Launch",
props: {
milestones: [
{label: "Idea", detail: "one sentence"},
{label: "Draft", detail: "scenes in place"},
{label: "Review", detail: "contract tests"},
{label: "Launch", detail: "one export"},
],
},
},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {Timeline} from "../components/timeline/timeline";
<Video>
<Scene id="timeline" duration="5s">
<Timeline
milestones={[
{
label: "Idea",
detail: "one sentence"
},
{
label: "Draft",
detail: "scenes in place"
},
{
label: "Review",
detail: "contract tests"
},
{
label: "Launch",
detail: "one export"
}
]}
headline="From idea to launch"
/>
</Scene>
</Video>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Narrative
- Recommended duration
- 150 frames · 5s
- Minimum duration
- 75 frames · 2.5s
- Entrance
- 72 frames
- Exit
- 0 frames
- Reduced motion
- the axis and markers appear at once
- Requires
- sans font
- Content limits
- milestones ≤ 5, labelLength ≤ 18
Props
| Prop | Type | Default | Required |
|---|---|---|---|
milestones | Milestone[] | — | Yes |
headline | string | — | — |
Related components
All componentsBefore and afterDirect visual comparison with a controlled divider.ComparisonTwo options contrasted through matched structure.Connection storyA left-to-right pipeline of connected stages.End cardThe closing frame: title, supporting line, and a call to action.Feature stackA sequence of features in one card.State transitionBefore, processing, and resolved product states.