Rolling number
Metrics roll without width changes or digit jitter.
Customize
import {Scene, Video} from "odori";
import {RollingNumber} from "../components/rolling-number/rolling-number";
<Video>
<Scene id="rolling-number" duration="3s">
<RollingNumber
from={0}
to={98.7}
label="frames delivered on time"
suffix="%"
decimals={1}
/>
</Scene>
</Video>Installation
pnpm odori add component rolling-numbernpx odori add component rolling-numberbunx odori add component rolling-numberEdit the source in videos/components/rolling-number/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/rolling-number/rolling-number.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type RollingNumberProps = {
from?: number;
to: number;
label?: string;
prefix?: string;
suffix?: string;
decimals?: number;
};
/**
* A number that counts without the width jitter proportional digits cause.
* Tabular figures hold every glyph in the same advance, so the label under it
* never shifts while the value climbs.
*/
export const RollingNumber = ({from = 0, to, label, prefix = "", suffix = "", decimals = 0}: RollingNumberProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const progress = interpolate(frame, [8, 62], [0, 1], {easing: Easing.standard});
const value = (from + (to - from) * progress).toFixed(decimals);
return (
<Fill
style={{
alignItems: "flex-start",
gap: 16 * scale,
justifyContent: "center",
padding: `${120 * scale}px ${150 * scale}px`,
}}
>
<div
style={{
fontSize: 208 * scale,
fontVariantNumeric: "tabular-nums",
fontWeight: 600,
letterSpacing: "-0.06em",
lineHeight: 1,
opacity: interpolate(frame, [0, 14], [0, 1], {easing: Easing.standard}),
}}
>
{prefix}
{value}
{suffix}
</div>
{label ? (
<div
style={{
color: brand.colors.muted,
fontSize: 40 * scale,
letterSpacing: "-0.015em",
opacity: interpolate(frame, [10, 30], [0, 1], {easing: Easing.standard}),
}}
>
{label}
</div>
) : null}
</Fill>
);
};
videos/components/rolling-number/rolling-number.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {RollingNumber} from "./rolling-number";
export default defineComponentPreview({
title: "Rolling number",
category: "Typography/Figures",
description: "Metrics roll without width changes or digit jitter.",
component: RollingNumber,
canvas: {width: 1920, height: 1080, duration: "3s"},
controls: {
from: {type: "number", defaultValue: 0, min: 0, max: 10000},
to: {type: "number", defaultValue: 98.7, min: 0, max: 10000},
decimals: {type: "number", defaultValue: 1, min: 0, max: 3},
label: {type: "text", defaultValue: "frames delivered on time"},
suffix: {type: "text", defaultValue: "%"},
},
examples: [
{name: "Percentage", props: {to: 98.7, decimals: 1, suffix: "%", label: "frames delivered on time"}},
{name: "Count", props: {to: 12480, decimals: 0, suffix: "", label: "frames rendered"}},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {RollingNumber} from "../components/rolling-number/rolling-number";
<Video>
<Scene id="rolling-number" duration="3s">
<RollingNumber
from={0}
to={98.7}
label="frames delivered on time"
suffix="%"
decimals={1}
/>
</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
- 54 frames · 1.8s
- Entrance
- 54 frames
- Exit
- 0 frames
- Reduced motion
- the final value is shown immediately
- Requires
- sans font
- Content limits
- label ≤ 48
Props
| Prop | Type | Default | Required |
|---|---|---|---|
from | number | 0 | — |
to | number | — | Yes |
label | string | — | — |
prefix | string | "" | — |
suffix | string | "" | — |
decimals | number | 0 | — |
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.