Studio KitAdvanced Text Animationsmotion/react

NumberTicker

Counts from a start value to a target with an expo-out ease when scrolled into view, with fixed decimals, tabular numerals, and optional prefix and suffix.

Preview

Customize

6 props
12480
0
1.6
0

Installation

Copies the component source and its CSS Module into your project.

npx motionfoundry add number-ticker

Writes components/ui/number-ticker.tsx and components/styles/number-ticker.module.css.

Usage

Updates live as you change the controls — only props that differ from the defaults are included.

example.tsx
1import { NumberTicker } from "@motionfoundry/ui";2 3export function Example() {4  return (5    <NumberTicker />6  );7}

Props

PropTypeDefault
value

Target value to count up to.

number12480
from

Starting value of the count.

number0
duration

Count duration in seconds.

number1.6
decimals

Number of fraction digits to display.

number0
prefix

Text rendered before the number (e.g. '$').

string''
suffix

Text rendered after the number (e.g. '%').

string'+'
className

Additional class merged onto the root element.

string''

Accessibility

The root exposes the final formatted value (with prefix and suffix) via role="img" and aria-label, so assistive tech never reads intermediate numbers. The server-rendered text is the final value; under prefers-reduced-motion it stays final with no count animation.

Toggle prefers-reduced-motion in your OS or browser devtools (Rendering → Emulate CSS media feature) to verify the reduced-motion state.

Source

components/ui/number-ticker.tsx
1"use client";2 3import * as React from "react";4import { animate, useInView, useReducedMotion } from "motion/react";5import styles from "../styles/number-ticker.module.css";6 7export interface NumberTickerProps {8  /** Target value to count up to */9  value?: number;10  /** Starting value of the count */11  from?: number;12  /** Count duration in seconds */13  duration?: number;14  /** Number of fraction digits to display */15  decimals?: number;16  /** Text rendered before the number */17  prefix?: string;18  /** Text rendered after the number */19  suffix?: string;20  /** Optional className */21  className?: string;22}23 24function format(n: number, decimals: number) {25  return n.toLocaleString("en-US", {26    minimumFractionDigits: decimals,27    maximumFractionDigits: decimals,28  });29}30 31export function NumberTicker({32  value = 12480,33  from = 0,34  duration = 1.6,35  decimals = 0,36  prefix = "",37  suffix = "+",38  className = "",39}: NumberTickerProps) {40  const rootRef = React.useRef<HTMLSpanElement>(null);41  const numRef = React.useRef<HTMLSpanElement>(null);42  const inView = useInView(rootRef, { once: true, margin: "0px 0px -10% 0px" });43  const prefersReducedMotion = useReducedMotion();44  const finalText = format(value, decimals);45 46  React.useEffect(() => {47    const node = numRef.current;48    if (!node) return;49    if (prefersReducedMotion) {50      node.textContent = finalText;51      return;52    }53    if (!inView) {54      node.textContent = format(from, decimals);55      return;56    }57    const controls = animate(from, value, {58      duration,59      ease: [0.16, 1, 0.3, 1],60      onUpdate: (v) => {61        node.textContent = format(v, decimals);62      },63    });64    return () => controls.stop();65  }, [inView, value, from, duration, decimals, prefersReducedMotion, finalText]);66 67  return (68    <span69      ref={rootRef}70      role="img"71      aria-label={`${prefix}${finalText}${suffix}`}72      className={`${styles.root} ${className}`.trim()}73    >74      {prefix ? (75        <span aria-hidden="true" className={styles.affix}>76          {prefix}77        </span>78      ) : null}79      <span ref={numRef} aria-hidden="true" className={styles.number}>80        {finalText}81      </span>82      {suffix ? (83        <span aria-hidden="true" className={styles.affix}>84          {suffix}85        </span>86      ) : null}87    </span>88  );89}