Number transition
A local count-up or count-down transition for a meaningful metric.
Live preview
Adjust a setting to update the preview and usage code together.
Usage & source
import type { CSSProperties } from "react";
import { NumberTransition } from "./components/stepkit/NumberTransition";
import "./components/stepkit/styles.css";
const themeStyle = {"--stepkit-bg":"#f7f7f8","--stepkit-surface":"#ffffff","--stepkit-surface-muted":"#f0f0f3","--stepkit-fg":"#17181c","--stepkit-muted":"#626572","--stepkit-border":"#e3e4e9","--stepkit-border-strong":"#c8cad3","--stepkit-code":"#f1f1f4","--stepkit-shadow":"0 20px 60px rgba(31, 26, 64, .10)","--stepkit-accent":"#6950df","--stepkit-accent-strong":"#5137c7","--stepkit-accent-soft":"#eeeafd","--stepkit-accent-on-soft":"#5137c7","--stepkit-accent-text":"#ffffff","--stepkit-error":"#b43145","--stepkit-placeholder":"#626572","--stepkit-radius":"16px","--stepkit-space":"16px","--stepkit-control-height":"44px","--stepkit-font-sans":"Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif"} as CSSProperties;
export default function Example() {
return (
<div className="stepkit-theme" data-stepkit-preset="modern" style={themeStyle}>
<NumberTransition
value={940}
startValue={0}
direction={"up"}
paused={false}
/>
</div>
);
}Dependencies
[email protected] [email protected]
/*
* Stepkit 0.2.0
* Copyright (c) 2026 Vladislav Stepanov
*
* Original Stepkit portions in this release are licensed under the MIT License. Adapted portions identify their upstream licence and copyright in the copied source and in `NOTICE-MAGICUI.md` where applicable.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*
* # Magic UI MIT licence snapshot
*
* Source: https://github.com/magicuidesign/magicui/blob/main/LICENSE.md
* Repository snapshot reviewed at commit: `bca54ab2730216c91e3a732f2af08e49c7ddebd4`
* Retrieved through Firecrawl: 2026-09-19
*
* MIT License
*
* Copyright (c) Magic UI
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
*/
"use client";
import { useEffect, useMemo, useState, type ComponentPropsWithoutRef } from "react";
import "./styles.css";
/**
* Adapted behaviour reference: Magic UI Number Ticker, MIT License.
* Copyright (c) Magic UI. Source and licence evidence: provenance/.
* Stepkit uses a local requestAnimationFrame implementation without motion.
*/
export interface NumberTransitionProps extends ComponentPropsWithoutRef<"span"> {
value: number;
startValue?: number;
direction?: "up" | "down";
delay?: number;
duration?: number;
decimalPlaces?: number;
paused?: boolean;
}
function clampNumber(value: number, fallback: number) {
return Number.isFinite(value) ? value : fallback;
}
function formatNumber(value: number, decimalPlaces: number) {
return new Intl.NumberFormat("en-US", {
minimumFractionDigits: decimalPlaces,
maximumFractionDigits: decimalPlaces,
}).format(Number(value.toFixed(decimalPlaces)));
}
export function NumberTransition({
value,
startValue = 0,
direction = "up",
delay = 0,
duration = 900,
decimalPlaces = 0,
paused = false,
className = "",
...props
}: NumberTransitionProps) {
const target = clampNumber(value, 0);
const origin = clampNumber(startValue, 0);
const decimals = Math.min(6, Math.max(0, Math.trunc(clampNumber(decimalPlaces, 0))));
const wait = Math.min(30_000, Math.max(0, clampNumber(delay, 0) * 1000));
const span = Math.min(20_000, Math.max(100, clampNumber(duration, 900)));
const initial = direction === "down" ? target : origin;
const finalValue = direction === "down" ? origin : target;
const [current, setCurrent] = useState(initial);
const [reducedMotion, setReducedMotion] = useState(false);
useEffect(() => {
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
const update = () => setReducedMotion(query.matches);
update();
query.addEventListener?.("change", update);
return () => query.removeEventListener?.("change", update);
}, []);
useEffect(() => {
setCurrent(initial);
if (paused || reducedMotion || typeof window === "undefined") return;
let frame = 0;
let timer: ReturnType<typeof setTimeout> | undefined;
const startedAt = performance.now() + wait;
const tick = (now: number) => {
const progress = Math.min(1, Math.max(0, (now - startedAt) / span));
const eased = 1 - Math.pow(1 - progress, 3);
setCurrent(initial + (finalValue - initial) * eased);
if (progress < 1) frame = window.requestAnimationFrame(tick);
};
if (wait > 0) timer = setTimeout(() => { frame = window.requestAnimationFrame(tick); }, wait);
else frame = window.requestAnimationFrame(tick);
return () => { if (timer) clearTimeout(timer); if (frame) window.cancelAnimationFrame(frame); };
}, [finalValue, initial, paused, reducedMotion, span, wait]);
const output = useMemo(() => formatNumber(paused || reducedMotion ? finalValue : current, decimals), [current, decimals, finalValue, paused, reducedMotion]);
return <span className={`stepkit stepkit-number-transition${className ? ` ${className}` : ""}`} data-direction={direction} data-paused={paused} {...props}>{output}</span>;
}
export default NumberTransition;
components/NumberTransition.tsxcomponents/styles.cssInstall
Use the released registry entry or download the complete archive.
npx [email protected] add https://vstepanov.com/stepkit/r/number-transition.jsonIntegration prompt
Install Stepkit Number transition 0.2.0 from the registry and preserve the included MIT notice. npx [email protected] add https://vstepanov.com/stepkit/v0.2.0/r/number-transition.json Install the tested runtime dependencies exactly: npm install [email protected] [email protected] Keep the component source, shared styles, NOTICE.md, and provenance files together. Apply the theme tokens on a wrapper rather than passing theme keys as component props. Usage: ```tsx import type { CSSProperties } from "react"; import { NumberTransition } from "./components/stepkit/NumberTransition"; import "./components/stepkit/styles.css"; const themeStyle = {"--stepkit-bg":"#f7f7f8","--stepkit-surface":"#ffffff","--stepkit-surface-muted":"#f0f0f3","--stepkit-fg":"#17181c","--stepkit-muted":"#626572","--stepkit-border":"#e3e4e9","--stepkit-border-strong":"#c8cad3","--stepkit-code":"#f1f1f4","--stepkit-shadow":"0 20px 60px rgba(31, 26, 64, .10)","--stepkit-accent":"#6950df","--stepkit-accent-strong":"#5137c7","--stepkit-accent-soft":"#eeeafd","--stepkit-accent-on-soft":"#5137c7","--stepkit-accent-text":"#ffffff","--stepkit-error":"#b43145","--stepkit-placeholder":"#626572","--stepkit-radius":"16px","--stepkit-space":"16px","--stepkit-control-height":"44px","--stepkit-font-sans":"Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif"} as CSSProperties; export default function Example() { return ( <div className="stepkit-theme" data-stepkit-preset="modern" style={themeStyle}> <NumberTransition value={940} startValue={0} direction={"up"} paused={false} /> </div> ); } ``` Selected bounded settings: {"value":940,"startValue":0,"direction":"up","paused":false}. Selected theme: {"mode":"light","accent":"violet","radius":"soft","density":"comfortable","preset":"modern"}.
Props
| Name | Type | Default | Description |
|---|---|---|---|
value | number | required | Target value. |
startValue | number | 0 | Starting value. |
direction | "up" | "down" | "up" | Direction of the transition. |
paused | boolean | false | Shows the static endpoint. |
decimalPlaces | number | 0 | Displayed decimal places. |
Accessibility & limits
- Outputs a native text span with tabular numerals.
- Reduced-motion preferences show the final value without animation.
Limitations
- Use for meaningful metrics rather than decoration.
Provenance
Adapted source under the MIT licence.
Upstream: Magic UI Number Ticker
Required notices: NOTICE.md, PROVENANCE-stepkit-number-transition.md, NOTICE-MAGICUI.md
- 2 source files in the release
- Version 0.2.0