https://github.com/serafimcloud/animated-blur-number
A React number that animates digit-by-digit with an iOS-style spring, blurring only the digits that change.
https://github.com/serafimcloud/animated-blur-number
Last synced: 15 days ago
JSON representation
A React number that animates digit-by-digit with an iOS-style spring, blurring only the digits that change.
- Host: GitHub
- URL: https://github.com/serafimcloud/animated-blur-number
- Owner: serafimcloud
- License: mit
- Created: 2026-06-02T20:30:16.000Z (16 days ago)
- Default Branch: main
- Last Pushed: 2026-06-02T20:50:08.000Z (16 days ago)
- Last Synced: 2026-06-02T22:26:18.786Z (16 days ago)
- Language: TypeScript
- Homepage: https://animated-blur-number.vercel.app
- Size: 1.43 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# animated-blur-number
A React number that transitions **digit-by-digit** with an iOS-style spring, and blurs **only the digits that actually change**. Unchanged digits stay crisp and perfectly still.
Inspired by iOS's `.contentTransition(.numericText())` and [motion.dev](https://motion.dev/docs/react-animate-number) - with the blur on top.

**Live demo:** [animated-blur-number.vercel.app](https://animated-blur-number.vercel.app)
## Why it looks good
- **Per-digit, diffed.** `1,204,837 → 1,204,974` animates only the trailing `837 → 974`; the `1,204,` never moves.
- **iOS spring.** The slide rides a `linear()` easing baked from a damped spring (subtle ~5% overshoot + settle), not a flat ease.
- **Blur scoped to change.** Each changing digit blurs as it travels and sharpens as it lands - like motion blur, only where something moved.
- **Directional.** Rolls up on increase, down on decrease.
- **SSR-safe.** Formats with a fixed locale (default `en-US`) so server and client agree - no hydration drift.
- **Accessible.** The full value is exposed to screen readers; the animated slots are `aria-hidden`. Honors `prefers-reduced-motion` (instant swap, no blur).
- **Zero runtime dependencies.** Just React - a single self-contained file. Styles are embedded and injected once at runtime, so there's no CSS file or config to wire up.
## Install
Copy the single file - [`src/components/animate-number.tsx`](src/components/animate-number.tsx) - into your project. No CSS file, no config: styles are embedded and injected at runtime. (React 18+.)
## Usage
```tsx
import { AnimateNumber } from "@/components/animate-number";
// plain integer (grouped)
// currency
// percentage, faster + blurrier
```
## Props
| Prop | Type | Default | Description |
| ----------- | --------------------------- | ----------- | ------------------------------------------------------------------ |
| `value` | `number` | - | The number to display. Changing it animates the affected digits. |
| `format` | `Intl.NumberFormatOptions` | `undefined` | Passed to `Intl.NumberFormat` (grouping, fraction digits, etc.). |
| `locale` | `string` | `"en-US"` | Formatting locale. Keep it fixed to avoid SSR hydration mismatches.|
| `prefix` | `React.ReactNode` | `undefined` | Rendered before the number (e.g. `"$"`). |
| `suffix` | `React.ReactNode` | `undefined` | Rendered after the number (e.g. `"%"`). |
| `duration` | `number` (ms) | `450` | Slide + blur duration. |
| `blur` | `number` (px) | `21` | Peak blur of a changing digit. `0` disables the blur. |
| `className` | `string` | `undefined` | Applied to the root; also forwards any other `` props. |
> Tip: use a fixed-width font feature for the cleanest motion. The CSS already sets `font-variant-numeric: tabular-nums` on the root.
## How it works
The formatted value is split into characters and rendered **right-aligned**, one slot per place value (keyed by its offset from the right, so the ones digit / decimal point keep a stable identity as the number grows or shrinks). A slot only mounts its animating layers when its own character differs from the previous one.
Each changing slot stacks two layers - the incoming digit and the outgoing one - and runs **two animations** at once:
- a **spring** on `transform` (the vertical slide + overshoot), and
- an **ease** on `opacity` + `filter: blur()` (the fade and de-blur).
Splitting them keeps the spring's overshoot out of opacity/blur (where values above 1 / below 0 would clamp). The spring is a `linear()` easing precomputed from a damped harmonic oscillator (`zeta = 0.68`), so it's pure CSS - no JS animation loop.
## Demo app
This repo is also a tiny Next.js app showcasing the component. Run it locally:
```bash
npm install
npm run dev
# open http://localhost:3000
```
## Recording the GIF
The demo GIF is produced from the running demo with Playwright + ffmpeg:
```bash
npm i -D playwright && npx playwright install chromium
npm run dev # in one terminal
DEMO_URL=http://localhost:3000 npm run record # in another
# then convert the webm:
ffmpeg -y -i recording/*.webm \
-vf "fps=24,scale=820:-1:flags=lanczos,split[s0][s1];[s0]palettegen=max_colors=128[p];[s1][p]paletteuse=dither=bayer" \
-loop 0 public/demo.gif
```
## License
[MIT](LICENSE) © [serafim](https://github.com/serafimcloud)