State transition
Before, processing, and resolved product states.
Customize
import {Scene, Video} from "odori";
import {StateTransition} from "../components/state-transition/state-transition";
<Video>
<Scene id="state-transition" duration="5s">
<StateTransition
from="Queued"
during="Rendering 360 frames"
to="Ready"
label="Export job"
/>
</Scene>
</Video>Installation
pnpm odori add component state-transitionnpx odori add component state-transitionbunx odori add component state-transitionEdit the source in videos/components/state-transition/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/state-transition/state-transition.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type StateTransitionProps = {
/** The state the product starts in. */
from: string;
/** What it looks like while the work happens. */
during: string;
/** Where it resolves. */
to: string;
label?: string;
};
/**
* Before, working, resolved. The card keeps its size across all three, so the
* transition reads as one object changing rather than three cards cutting.
*/
export const StateTransition = ({from, during, to, label}: StateTransitionProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const stage = frame < 34 ? 0 : frame < 78 ? 1 : 2;
const text = [from, during, to][stage];
const enterAt = [0, 34, 78][stage];
const enter = interpolate(frame, [enterAt, enterAt + 14], [0, 1], {easing: Easing.standard});
const tint = stage === 2 ? brand.colors.accent : brand.colors.border;
const spin = (frame % 30) / 30;
return (
<Fill style={{alignItems: "center", gap: 30 * scale, justifyContent: "center"}}>
{label ? <div style={{color: brand.colors.muted, fontSize: 32 * scale}}>{label}</div> : null}
<div
style={{
alignItems: "center",
background: brand.colors.surface,
border: `${2 * scale}px solid ${tint}`,
borderRadius: 26 * scale,
display: "flex",
gap: 22 * scale,
minWidth: 720 * scale,
justifyContent: "center",
padding: `${44 * scale}px ${52 * scale}px`,
}}
>
{stage === 1 ? (
<span
style={{
border: `${4 * scale}px solid ${brand.colors.border}`,
borderRadius: 999,
borderTopColor: brand.colors.accent,
display: "block",
height: 46 * scale,
transform: `rotate(${spin * 360}deg)`,
width: 46 * scale,
}}
/>
) : null}
<span
style={{
fontSize: 62 * scale,
fontWeight: 560,
letterSpacing: "-0.03em",
opacity: enter,
transform: `translateY(${(1 - enter) * 14 * scale}px)`,
}}
>
{text}
</span>
</div>
</Fill>
);
};
videos/components/state-transition/state-transition.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {StateTransition} from "./state-transition";
export default defineComponentPreview({
title: "State transition",
category: "Narrative",
description: "Before, processing, and resolved product states.",
component: StateTransition,
canvas: {width: 1920, height: 1080, duration: "5s"},
controls: {
label: {type: "text", defaultValue: "Export job"},
from: {type: "text", defaultValue: "Queued", maxLength: 24},
during: {type: "text", defaultValue: "Rendering 360 frames", maxLength: 32},
to: {type: "text", defaultValue: "Ready", maxLength: 24},
},
examples: [{name: "Export", props: {}}],
});
Add it to a scene
import {Scene, Video} from "odori";
import {StateTransition} from "../components/state-transition/state-transition";
<Video>
<Scene id="state-transition" duration="5s">
<StateTransition
from="Queued"
during="Rendering 360 frames"
to="Ready"
label="Export job"
/>
</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
- 120 frames · 4s
- Entrance
- 14 frames
- Exit
- 0 frames
- Reduced motion
- states cut without a spinner
- Requires
- sans font
- Content limits
- from ≤ 24, during ≤ 32, to ≤ 24
Props
| Prop | Type | Default | Required |
|---|---|---|---|
fromThe state the product starts in. | string | — | Yes |
duringWhat it looks like while the work happens. | string | — | Yes |
toWhere it resolves. | string | — | Yes |
label | 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.TimelineMilestones revealed along a timeline.