Workflow steps
Sequential process with one active focal point.
Customize
import {Scene, Video} from "odori";
import {WorkflowSteps} from "../components/workflow-steps/workflow-steps";
<Video>
<Scene id="workflow-steps" duration="6s">
<WorkflowSteps
steps={[
"Author",
"Preview",
"Test",
"Export"
]}
headline="From file to finished cut"
hold={40}
/>
</Scene>
</Video>Installation
pnpm odori add component workflow-stepsnpx odori add component workflow-stepsbunx odori add component workflow-stepsEdit the source in videos/components/workflow-steps/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/workflow-steps/workflow-steps.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale, useVideo} from "odori";
export type WorkflowStepsProps = {
steps: string[];
headline?: string;
/** Frames each step holds the focus. */
hold?: number;
};
/**
* A sequential process with exactly one active step. Completed steps stay on
* screen at reduced weight, so the viewer keeps the whole path in view while
* the focus advances.
*/
export const WorkflowSteps = ({steps, headline, hold = 40}: WorkflowStepsProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const {width, height} = useVideo();
const stacked = height >= width;
const active = Math.min(steps.length - 1, Math.floor(Math.max(0, frame - 14) / hold));
return (
<Fill style={{gap: 46 * 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={{alignItems: "stretch", display: "flex", flexDirection: stacked ? "column" : "row", gap: 20 * scale}}>
{steps.map((step, index) => {
const enter = interpolate(frame, [10 + index * 6, 10 + index * 6 + 18], [0, 1], {easing: Easing.standard});
const isActive = index === active;
const done = index < active;
return (
<div
key={step}
style={{
alignItems: "center",
background: isActive ? `color-mix(in srgb, ${brand.colors.accent} 14%, ${brand.colors.surface})` : brand.colors.surface,
border: `1px solid ${isActive ? brand.colors.accent : brand.colors.border}`,
borderRadius: 22 * scale,
display: "flex",
flex: 1,
gap: 18 * scale,
opacity: enter * (done ? 0.6 : 1),
padding: `${26 * scale}px ${28 * scale}px`,
transform: `translateY(${(1 - enter) * 16 * scale}px)`,
}}
>
<span
style={{
alignItems: "center",
background: isActive ? brand.colors.accent : "transparent",
border: `${2 * scale}px solid ${isActive ? brand.colors.accent : brand.colors.border}`,
borderRadius: 999,
color: isActive ? brand.colors.background : brand.colors.muted,
display: "flex",
fontFamily: brand.typography.mono,
fontSize: 24 * scale,
height: 46 * scale,
justifyContent: "center",
width: 46 * scale,
}}
>
{index + 1}
</span>
<span style={{fontSize: 34 * scale, letterSpacing: "-0.02em"}}>{step}</span>
</div>
);
})}
</div>
</Fill>
);
};
videos/components/workflow-steps/workflow-steps.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {WorkflowSteps} from "./workflow-steps";
export default defineComponentPreview({
title: "Workflow steps",
category: "Narrative",
description: "Sequential process with one active focal point.",
component: WorkflowSteps,
canvas: {width: 1920, height: 1080, duration: "6s"},
controls: {
headline: {type: "text", defaultValue: "From file to finished cut"},
hold: {type: "number", defaultValue: 40, min: 16, max: 90},
},
examples: [
{name: "Pipeline", props: {steps: ["Author", "Preview", "Test", "Export"]}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {WorkflowSteps} from "../components/workflow-steps/workflow-steps";
<Video>
<Scene id="workflow-steps" duration="6s">
<WorkflowSteps
steps={[
"Author",
"Preview",
"Test",
"Export"
]}
headline="From file to finished cut"
hold={40}
/>
</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
- 180 frames · 6s
- Minimum duration
- 90 frames · 3s
- Entrance
- 30 frames
- Exit
- 0 frames
- Reduced motion
- every step shows without a moving focus
- Requires
- sans font, mono font
- Content limits
- steps ≤ 5, stepLength ≤ 26
Props
| Prop | Type | Default | Required |
|---|---|---|---|
steps | string[] | — | Yes |
headline | string | — | — |
holdFrames each step holds the focus. | number | 40 | — |
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.