Code walkthrough
Progressive code reveal with authored focal regions.
Customize
import {Scene, Video} from "odori";
import {CodeWalkthrough} from "../components/code-walkthrough/code-walkthrough";
<Video>
<Scene id="code-walkthrough" duration="8s">
<CodeWalkthrough
code={`export const metadata = defineVideoMetadata({
id: "launch",
duration: "8s",
});
export default function LaunchVideo() {
return (
<Video>
<Scene duration="5s">
<TitleReveal title="Ship the story." />
</Scene>
</Video>
);
}`}
steps={[
{
from: 1,
to: 4,
note: "Static metadata, so discovery never runs your component."
},
{
from: 6,
to: 13,
note: "Ordinary JSX for the timeline."
},
{
from: 8,
to: 10,
note: "Scenes are ordered, not numbered."
}
]}
title="videos/launch/video.tsx"
hold={60}
/>
</Scene>
</Video>Installation
pnpm odori add component code-walkthroughnpx odori add component code-walkthroughbunx odori add component code-walkthroughEdit the source in videos/components/code-walkthrough/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/code-walkthrough/code-walkthrough.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type WalkthroughStep = {
/** Inclusive line range to focus, one-based. */
from: number;
to: number;
note?: string;
};
export type CodeWalkthroughProps = {
code: string;
steps: WalkthroughStep[];
title?: string;
/** Frames each step holds before the next takes focus. */
hold?: number;
};
/**
* Source read in authored order. Every line stays on screen the whole time, so
* the reader keeps their place; focus moves by dimming everything the current
* step is not about.
*/
export const CodeWalkthrough = ({code, steps, title = "videos/launch/video.tsx", hold = 60}: CodeWalkthroughProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const lines = code.split("\n");
const index = Math.min(steps.length - 1, Math.floor(Math.max(0, frame - 12) / hold));
const step = steps[index];
const stepStart = 12 + index * hold;
const focus = interpolate(frame, [stepStart, stepStart + 16], [0, 1], {easing: Easing.standard});
return (
<Fill style={{alignItems: "center", justifyContent: "center", padding: 90 * scale}}>
<div
style={{
background: brand.colors.surface,
border: `1px solid ${brand.colors.border}`,
borderRadius: 26 * scale,
minWidth: 1180 * scale,
overflow: "hidden",
}}
>
<div
style={{
borderBottom: `1px solid ${brand.colors.border}`,
color: brand.colors.muted,
fontFamily: brand.typography.mono,
fontSize: 24 * scale,
padding: `${20 * scale}px ${28 * scale}px`,
}}
>
{title}
</div>
<div style={{padding: `${20 * scale}px 0`}}>
{lines.map((line, lineIndex) => {
const number = lineIndex + 1;
const inFocus = step && number >= step.from && number <= step.to;
const dim = inFocus ? 1 : 1 - 0.68 * focus;
return (
<div
key={`${line}-${lineIndex}`}
style={{
alignItems: "center",
background: inFocus
? `color-mix(in srgb, ${brand.colors.accent} ${focus * 10}%, transparent)`
: "transparent",
display: "flex",
gap: 24 * scale,
opacity: dim,
padding: `${6 * scale}px ${28 * scale}px`,
}}
>
<span style={{color: brand.colors.muted, fontFamily: brand.typography.mono, fontSize: 24 * scale, width: 44 * scale}}>
{number}
</span>
<span style={{fontFamily: brand.typography.mono, fontSize: 30 * scale, whiteSpace: "pre"}}>{line}</span>
</div>
);
})}
</div>
{step?.note ? (
<div
style={{
borderTop: `1px solid ${brand.colors.border}`,
color: brand.colors.muted,
fontSize: 30 * scale,
opacity: focus,
padding: `${22 * scale}px ${28 * scale}px`,
}}
>
{step.note}
</div>
) : null}
</div>
</Fill>
);
};
videos/components/code-walkthrough/code-walkthrough.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {CodeWalkthrough} from "./code-walkthrough";
export default defineComponentPreview({
title: "Code walkthrough",
category: "Developer proof/Code",
description: "Progressive code reveal with authored focal regions.",
component: CodeWalkthrough,
canvas: {width: 1920, height: 1080, duration: "8s"},
controls: {
title: {type: "text", defaultValue: "videos/launch/video.tsx"},
hold: {type: "number", defaultValue: 60, min: 24, max: 120},
},
examples: [
{
name: "Metadata to timeline",
props: {
code: `export const metadata = defineVideoMetadata({
id: "launch",
duration: "8s",
});
export default function LaunchVideo() {
return (
<Video>
<Scene duration="5s">
<TitleReveal title="Ship the story." />
</Scene>
</Video>
);
}`,
steps: [
{from: 1, to: 4, note: "Static metadata, so discovery never runs your component."},
{from: 6, to: 13, note: "Ordinary JSX for the timeline."},
{from: 8, to: 10, note: "Scenes are ordered, not numbered."},
],
},
},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {CodeWalkthrough} from "../components/code-walkthrough/code-walkthrough";
<Video>
<Scene id="code-walkthrough" duration="8s">
<CodeWalkthrough
code={`export const metadata = defineVideoMetadata({
id: "launch",
duration: "8s",
});
export default function LaunchVideo() {
return (
<Video>
<Scene duration="5s">
<TitleReveal title="Ship the story." />
</Scene>
</Video>
);
}`}
steps={[
{
from: 1,
to: 4,
note: "Static metadata, so discovery never runs your component."
},
{
from: 6,
to: 13,
note: "Ordinary JSX for the timeline."
},
{
from: 8,
to: 10,
note: "Scenes are ordered, not numbered."
}
]}
title="videos/launch/video.tsx"
hold={60}
/>
</Scene>
</Video>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Developer proof
- Recommended duration
- 240 frames · 8s
- Minimum duration
- 120 frames · 4s
- Entrance
- 28 frames
- Exit
- 0 frames
- Reduced motion
- all steps show without dimming
- Requires
- mono font
- Content limits
- lines ≤ 16, lineLength ≤ 62, steps ≤ 4
Props
| Prop | Type | Default | Required |
|---|---|---|---|
code | string | — | Yes |
steps | WalkthroughStep[] | — | Yes |
title | string | "videos/launch/video.tsx" | — |
holdFrames each step holds before the next takes focus. | number | 60 | — |
Related components
All componentsProduct launchA framed source excerpt that reveals line by line.Dependency graphPackages and modules connected as a readable graph.Diff proofBefore and after source changes with semantic emphasis.File treeProgressive repository navigation with active file state.Log streamPaced runtime logs with levels and bounded scrolling.TerminalA typed command session with deterministic output reveals.