Skip to content

Counter

A number that counts up, down or rolls to its new value, formatted for the provider's locale.

Use it where a number's change is the news: a price switching billing periods, a metric on a dashboard, a stat as it scrolls into view. Don't animate numbers people read in bulk or compare across rows, like table cells, and don't count a value the reader needs immediately, like a balance at checkout. Show those as plain text.

Installation

npm i @youj/motion

import { Counter } from '@youj/motion'

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Usage

import { Counter } from '@youj/motion'

export function Revenue({ total }: { total: number }) {
  return <Counter value={total} format={{ style: 'currency', currency: 'USD', maximumFractionDigits: 0 }} />
}

Formatting is plain Intl.NumberFormat, so currency, percent, compact notation, units and fraction digits all work, in any locale. Without one, it formats in the YoujProvider's locale (en-US without a provider). Numerals are tabular, so the width holds steady while the digits change.

Examples

Two modes

tween, the default, counts through every value in between. It suits a number's first arrival.

roll spins each digit in its own column, like an odometer. Columns are keyed by place value, so they keep their position as the number grows a digit. It suits values that update while you watch.

Reference

Propsvalue, from, mode… (8)

Props

Counter props
PropType
valueRequirednumber–
fromnumber

Starting value for the first play.

default 0

0
mode'tween' | 'roll'

'tween' counts through values; 'roll' spins each digit like an odometer.

default 'tween'

'tween'
formatIntl.NumberFormatOptions

Intl.NumberFormat options: currency, percent, compact notation, fraction digits.

–
localestring

A BCP 47 locale. Default: the YoujProvider locale, en-US without one.

–
inViewInView

When the first play starts: false at once; true (the default) when scrolled into view, at 50% visible; or { amount, margin } (it plays once).

–
durationDurationToken
'instant''fast''base''slow''deliberate'

Tween mode's duration. Roll mode rolls on the smooth spring.

default 'deliberate'

'deliberate'
easeEasingToken
'standard''enter''exit''emphasized''linear'

Tween mode's curve.

default 'emphasized'

'emphasized'
cyclenumber

Roll mode, for a value that wraps around (60 for seconds or minutes, 24 for hours): a change of more than half the cycle is read as a wrap, so 00 → 59 rolls one step back, and the tens wheel holds only the digits the cycle reaches (0–5 for 60).

–
Other props pass through: extends HTMLAttributes<HTMLSpanElement>.
MotionCount, roll, column offset · reduced motion

Motion

Motion tokens
MomentTokens
Counttween mode
duration.deliberateease.emphasized

560ms · cubic-bezier(0.6, 0, 0.1, 1)

560mscubic-bezier(0.6, 0, 0.1, 1)
Rollroll mode, each digit column
duration.deliberateease.emphasized

560ms · cubic-bezier(0.6, 0, 0.1, 1)

560mscubic-bezier(0.6, 0, 0.1, 1)
Column offsetper place value, ones first
stagger.tight

24ms step

24ms step

Reduced motion

Shows the formatted value immediately. New values swap in place.

The emphasized curve winds up briefly, moves fast through the middle and lands slowly, so the final digits are readable before the number stops.

Reduced motion

The formatted value shows at once, with no count and no roll. A new value swaps in place. The number is the information, and it's never delayed.

AccessibilityARIA, announcements

Accessibility

  • The formatted value is always in the document, in a visually hidden span, and the moving digits are aria-hidden. A screen reader hears "$48,210", never a run of intermediate numbers.
  • A counter doesn't announce changes on its own. If a live value matters, wrap it in a live region such as role="status", and it's announced once, at its final value.