Value swap
Replace one label or value while preserving alignment.
Customize
import {Scene, Video} from "odori";
import {ValueSwap} from "../components/value-swap/value-swap";
<Video>
<Scene id="value-swap" duration="3s">
<ValueSwap
label="Status"
from="Draft"
to="Ready"
swapFrame={45}
/>
</Scene>
</Video>Installation
pnpm odori add component value-swapnpx odori add component value-swapbunx odori add component value-swapEdit the source in videos/components/value-swap/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/value-swap/value-swap.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type ValueSwapProps = {
label?: string;
from: string;
to: string;
/** Frame the swap happens on. */
swapFrame?: number;
};
/**
* One value replaced by another in place. Both values occupy the same grid
* cell, so the swap cannot shift anything around it, however wide the new
* value is.
*/
export const ValueSwap = ({label, from, to, swapFrame = 45}: ValueSwapProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const out = interpolate(frame, [swapFrame - 10, swapFrame], [0, 1], {
easing: Easing.standard,
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const into = interpolate(frame, [swapFrame, swapFrame + 14], [0, 1], {
easing: Easing.standard,
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const cell = {gridColumn: 1, gridRow: 1} as const;
return (
<Fill
style={{
alignItems: "center",
gap: 24 * scale,
justifyContent: "center",
padding: `${120 * scale}px ${140 * scale}px`,
}}
>
{label ? (
<div
style={{
color: brand.colors.muted,
fontSize: 34 * scale,
opacity: interpolate(frame, [0, 14], [0, 1], {easing: Easing.standard}),
}}
>
{label}
</div>
) : null}
<div style={{display: "grid", placeItems: "center"}}>
<span
style={{
...cell,
fontSize: 120 * scale,
fontWeight: 570,
letterSpacing: "-0.05em",
opacity: 1 - out,
transform: `translateY(${out * -34 * scale}px)`,
}}
>
{from}
</span>
<span
style={{
...cell,
color: brand.colors.accent,
fontSize: 120 * scale,
fontWeight: 600,
letterSpacing: "-0.05em",
opacity: into,
transform: `translateY(${(1 - into) * 34 * scale}px)`,
}}
>
{to}
</span>
</div>
</Fill>
);
};
videos/components/value-swap/value-swap.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {ValueSwap} from "./value-swap";
export default defineComponentPreview({
title: "Value swap",
category: "Typography/Figures",
description: "Replace one label or value while preserving alignment.",
component: ValueSwap,
canvas: {width: 1920, height: 1080, duration: "3s"},
controls: {
label: {type: "text", defaultValue: "Status"},
from: {type: "text", defaultValue: "Draft", maxLength: 24},
to: {type: "text", defaultValue: "Ready", maxLength: 24},
swapFrame: {type: "number", defaultValue: 45, min: 12, max: 80},
},
examples: [
{name: "Status", props: {label: "Status", from: "Draft", to: "Ready"}},
{name: "Duration", props: {label: "Render time", from: "8 min", to: "40 s"}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {ValueSwap} from "../components/value-swap/value-swap";
<Video>
<Scene id="value-swap" duration="3s">
<ValueSwap
label="Status"
from="Draft"
to="Ready"
swapFrame={45}
/>
</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
- 90 frames · 3s
- Minimum duration
- 60 frames · 2s
- Entrance
- 14 frames
- Exit
- 0 frames
- Reduced motion
- the values cross-fade without travel
- Requires
- sans font
- Content limits
- from ≤ 24, to ≤ 24
Props
| Prop | Type | Default | Required |
|---|---|---|---|
label | string | — | — |
from | string | — | Yes |
to | string | — | Yes |
swapFrameFrame the swap happens on. | number | 45 | — |
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.Focus inWords settle from a soft blur into a crisp line.Glitch textA title that arrives through brief seeded interference bursts.