vnhax
Motion & DisplayMagicUI PatternWCAG 2.1 AA Compliant · Zero-Dependency

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 live

Numbers count up once when they scroll into view. Example figures only.

48,250
Monthly readers
99.98%
Uptime this year
12,400+
Open-source stars
Engineering Specification

Component 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.

CLI Installation CommandShadcn / MagicUI Compatible
npx shadcn@latest add "https://magicui.design/r/number-ticker"

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.

PropTypeDefaultDescription
valuenumberRequiredThe final number to count up to.
decimalsnumber0Digits after the decimal point, e.g. 2 for 99.98.
durationnumber (ms)1400Length of the count-up animation.
localestring'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

Cumulative Layout Shift
0.00 (tabular-nums keeps the width constant while counting)
Interaction to Next Paint
Animation runs only once, after the element is visible

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.

High-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.