Number Ticker
Animated count-up for stats and KPIs that starts when visible, keeps width stable and reads the final value to screen readers.
Interactive Live Preview
Test responsive viewport scaling & interactions liveComponent Architecture & Rationale
The Number Ticker counts a number up from zero to its final value, the familiar effect on landing-page stat rows ("48,250 readers", "99.98% uptime"). This version starts only when the number scrolls into view, uses an ease-out curve so it slows down near the end, formats with locale separators and decimals, and shows the final value immediately to people who prefer reduced motion.
An IntersectionObserver waits until the element is at least 40% visible, then disconnects so the animation runs once. A requestAnimationFrame loop computes progress from elapsed time (not frame count), applies an ease-out cubic curve, and formats the value with toLocaleString. tabular-nums keeps every digit the same width so the layout does not wobble while counting. The animated digits are aria-hidden and a visually hidden copy holds the final value, so screen readers announce the real number once instead of every intermediate frame.
Key Technical Features
- ✓Starts on scroll into view with IntersectionObserver, runs once
- ✓Time-based easing with requestAnimationFrame, smooth at any frame rate
- ✓Locale formatting with thousands separators and fixed decimals
- ✓tabular-nums keeps width stable during the count
- ✓Reduced-motion users and screen readers get the final value directly
Complete Production Source Code
Standalone, dependency-free implementation. Drop directly into your project.
1. React 19 / TypeScript Component (NumberTicker.tsx)
TypeScript + Semantic JSX'use client';
import React, { useEffect, useRef, useState } from 'react';
type NumberTickerProps = { value: number; decimals?: number; duration?: number; locale?: string };
export function NumberTicker({ value, decimals = 0, duration = 1400, locale = 'en-US' }: NumberTickerProps) {
const ref = useRef<HTMLSpanElement>(null);
const [display, setDisplay] = useState(0);
const format = (n: number) =>
n.toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
useEffect(() => {
const el = ref.current;
if (!el) return;
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
setDisplay(value);
return;
}
let frame = 0;
const observer = new IntersectionObserver(([entry]) => {
if (!entry.isIntersecting) return;
observer.disconnect();
const start = performance.now();
const tick = (now: number) => {
const t = Math.min((now - start) / duration, 1);
setDisplay(value * (1 - Math.pow(1 - t, 3))); // ease-out cubic
if (t < 1) frame = requestAnimationFrame(tick);
};
frame = requestAnimationFrame(tick);
}, { threshold: 0.4 });
observer.observe(el);
return () => { observer.disconnect(); cancelAnimationFrame(frame); };
}, [value, duration]);
return (
<span className="number-ticker">
<span ref={ref} aria-hidden="true">{format(display)}</span>
<span className="sr-only">{format(value)}</span>
</span>
);
}2. Vanilla CSS Stylesheet (Zero-Dependency)
Native CSS Tokens & Transitions.number-ticker {
font-variant-numeric: tabular-nums; /* equal-width digits: no wobble while counting */
letter-spacing: -0.02em;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}3. Tailwind CSS Classes Variant
Tailwind v3 & v4 Utility Equivalents<!-- Tailwind markup for the stat card; the counting needs the React (or vanilla JS) logic -->
<div class="rounded-2xl border border-slate-200 bg-white p-5">
<p class="text-3xl font-extrabold tabular-nums tracking-tight text-slate-900">
<span aria-hidden="true" data-ticker-value="48250">0</span><span class="sr-only">48,250</span>
</p>
<p class="mt-1 text-sm text-slate-500">Monthly readers</p>
</div>Installation & Integration Guide
Steps to add this component to Next.js (App Router), Vite, or Astro projects.
Step 1: Copy the component
# No dependencies. Copy NumberTicker.tsx into your components folder,
# or use the MagicUI registry command above if you already use shadcn/ui.Step 2: Use it in a stats row
Only animate numbers that are true. A count-up makes a stat more noticeable, so make sure it is accurate and up to date.
import { NumberTicker } from '@/components/NumberTicker';
<p className="stat">
<NumberTicker value={99.98} decimals={2} />%
<span>uptime this year</span>
</p>Props & API Reference
TypeScript interfaces, default values, and runtime behavior.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | number | Required | The final number to count up to. |
| decimals | number | 0 | Digits after the decimal point, e.g. 2 for 99.98. |
| duration | number (ms) | 1400 | Length of the count-up animation. |
| locale | string | 'en-US' | Locale for number formatting, e.g. "de-DE" for 48.250. |
Accessibility & Core Web Vitals Audit
Screen-reader safe and reduced-motion aware
ARIA Semantic Annotations
- The animated digits are aria-hidden; a visually hidden span holds the final value, so assistive tech reads the real number once.
- Users with prefers-reduced-motion see the final value immediately with no animation.
Keyboard Navigation
- The ticker is not interactive, so it adds no tab stops.
Core Web Vitals Impact
Engineering Tip: Render the final value on the server and animate only on the client after hydration, so search engines and no-JS visitors still see the real number.
Frequently Asked Questions
How do I start the count-up only when the number is visible?
Use an IntersectionObserver. When the element becomes visible (the demo uses 40%), start a requestAnimationFrame loop and disconnect the observer so it runs only once.
Why does my counter make the layout jump?
Proportional fonts give digits different widths, so the number changes width as it counts. Set font-variant-numeric: tabular-nums (Tailwind: tabular-nums) to make every digit the same width.
Will screen readers read every number as it counts?
They can if the counting text is exposed. Mark the animated text aria-hidden and put the final value in a visually hidden span, as in the code above.
Is it okay to animate made-up stats?
No. A count-up draws attention to the number, so it must be accurate. Showing invented figures as real ones is misleading to visitors and can break advertising policies.
More UI Components & Patterns
Tweet Card
Verified social testimonial card with live engagement metrics, media embed, and responsive theme support.
Inspect Component Layouts & GridsBento Grid
Asymmetric feature showcase grid with ambient radial spotlight, badge headers, and interactive hover states.
Inspect Component Animations & FeedsAnimated List
Dynamic notification and activity feed with staggered entrance transitions, auto-flow, and pause controls.
Inspect ComponentHigh-Quality Interface Engineering
Every pattern in the VNHAX component directory is designed for longevity. By eliminating heavy JavaScript styling runtimes and adhering strictly to semantic HTML, your application maintains zero-shift Core Web Vitals and frictionless accessibility compliance.