<!-- Protocore Design System — RollingNumber -->
# RollingNumber

- **Category:** Data Display (`data-display`)
- **Slug:** `data-display/rolling-number`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { RollingNumber } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/data-display/rolling-number

> A tabular, monospaced counter whose digits roll like an odometer to a new value, honoring reduced motion and Intl formatting.

## How the roll works

Each ASCII digit is an **odometer column**: a 0–9 strip clipped to a one-glyph window, translated on the Y axis to show the current digit. When `value` changes, React re-renders the new translate offset and CSS transitions the strip — so `3 → 7` visibly rolls through 4, 5, 6. The value is also emitted once as visually-hidden text, so assistive tech reads the number, not the digit ladder. There is **no rAF loop**; the animation is declarative and therefore SSR-safe and reduced-motion-safe (the global motion rule collapses the transition to a snap).

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `className` | `string` | no | — | — |
| `duration` | `number` | no | `800` | Roll duration in milliseconds. Ignored under reduced motion (snaps). |
| `format` | `NumberFormatOptions` | no | — | `Intl.NumberFormat` options — currency, compact notation, min/max fraction digits, etc. The formatted string is rolled digit-by-digit. |
| `locale` | `string` | no | `en` | BCP-47 locale, fixed so SSR and hydration agree. |
| `signal` | `boolean` | no | — | Paint the number in the reserved accent — the single-live-number signal role. |
| `style` | `CSSProperties` | no | — | — |
| `value` | `number` | yes | — | The number to display. Digits roll from the previous value to this one. |

## Examples

### Live counter

Change `value` and each digit column rolls from its old glyph to the new one. The animation is pure CSS transform — the component server-renders the final value, so it's correct with no JS and snaps instantly under reduced motion.

```tsx
import { RollingNumber, Button, HStack, VStack, Text } from "@protocore/pds";
import { useState } from "react";

export default function Demo() {
  const [value, setValue] = useState(8_241_003);

  return (
    <VStack gap={5} align="start">
      <div style={{ fontSize: 40, fontWeight: 600 }}>
        <RollingNumber value={value} aria-label="Block height" />
      </div>
      <HStack gap={3} align="center" wrap>
        <Button variant="secondary" onClick={() => setValue((v) => v + 1)}>
          +1 block
        </Button>
        <Button variant="secondary" onClick={() => setValue((v) => v + 1_337)}>
          +1,337
        </Button>
        <Button variant="ghost" onClick={() => setValue(8_241_003)}>
          Reset
        </Button>
        <Text size="sm" color="muted">
          Digits roll from the old value to the new one.
        </Text>
      </HStack>
    </VStack>
  );
}
```

### Formatting

Pass `Intl.NumberFormat` options through `format` — compact notation, currency, fixed fraction digits — and a `locale`. Grouping separators and currency symbols render as static glyphs between the rolling digit columns.

```tsx
import { RollingNumber, Button, HStack, VStack, Text } from "@protocore/pds";
import { useState } from "react";

export default function Demo() {
  const [n, setN] = useState(0);
  // A small orbit of realistic figures to roll between.
  const revenue = [49_900, 128_400, 1_204_000, 3_880_500][n % 4] ?? 0;
  const throughput = [1_200, 18_600, 1_240_000, 42_000_000][n % 4] ?? 0;

  return (
    <VStack gap={5} align="start">
      <VStack gap={2} align="start">
        <Text mono size="sm" color="muted">
          MRR (EUR, compact)
        </Text>
        <div style={{ fontSize: 32, fontWeight: 600 }}>
          <RollingNumber
            value={revenue}
            format={{
              style: "currency",
              currency: "EUR",
              notation: "compact",
              maximumFractionDigits: 1,
            }}
            aria-label="Monthly recurring revenue"
          />
        </div>
      </VStack>
      <VStack gap={2} align="start">
        <Text mono size="sm" color="muted">
          Requests / min
        </Text>
        <div style={{ fontSize: 32, fontWeight: 600 }}>
          <RollingNumber
            value={throughput}
            format={{ notation: "compact" }}
            aria-label="Requests per minute"
          />
        </div>
      </VStack>
      <HStack gap={3}>
        <Button variant="secondary" onClick={() => setN((v) => v + 1)}>
          Next figures
        </Button>
      </HStack>
    </VStack>
  );
}
```

## Do & don't

**Do**

- Use it for a single, prominent live figure — active sessions, block height, throughput.
- Format with `Intl` options rather than pre-formatting the string yourself.
- Give it an `aria-label` (or nearby label) describing what the number measures.
- Reserve `signal` for the one number that is genuinely live on the surface.

**Don't**

- Don't roll a whole table of numbers — the motion stops being a signal and becomes noise.
- Don't feed it a pre-formatted string; pass the raw `number` and a `format`.
- Don't use `signal` as a general highlight — the accent budget is under 1%.
- Don't animate values that update many times per second; throttle upstream first.

## Accessibility

**Notes**

- The rolling digit columns are `aria-hidden`; the formatted value is exposed once as visually-hidden text so screen readers announce the number cleanly.
- Under `prefers-reduced-motion: reduce` the roll collapses to an instant snap via the design system's base motion rule — the final value is always shown.
- Server-rendering emits the final value, so the number is correct before (and without) hydration.

## Related

`money-amount`, `metric-delta`, `signal-count`, `sparkline`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
