Home / Snippets / Animation /

Counter animation

Animate numbers without JavaScript using a digit “roller”: a vertical strip translated with steps() so digits snap crisply.

Requests

Rolling counter (CSS only)

01234567890
01234567890
01234567890
01234567890
Widely Supported
animationno-js

Quick implementation

/* One digit “window” */
.digit {
  --digit-h: 2.25rem;
  height: var(--digit-h);
  overflow: hidden;
}

/* Vertical strip of 0..9 */
.strip {
  display: grid;
  grid-auto-rows: var(--digit-h);
  animation: roll 2.5s steps(10) infinite;
}

@keyframes roll {
  to { transform: translateY(calc(var(--digit-h) * -10)); }
}

@media (prefers-reduced-motion: reduce) {
  .strip { animation: none; }
}

Prompt this to your LLM

Includes role, constraints, two framework variants, and edge cases to handle.

You are a senior frontend engineer building a CSS-only animated counter.

Goal: Animate a numeric counter using a digit roller effect (0-9) with crisp snapping, no JavaScript.

Technical constraints:
- Each digit uses a fixed-height window and an inner vertical strip of digits 0..9 (optionally repeat 0 at the end).
- Animate the strip with transform: translateY(...) and steps(10) so the digits snap instead of easing between values.
- Use a CSS custom property (e.g. --digit-h) for digit height so sizing scales cleanly.
- Stagger different digits using different animation-duration values (or animation-delay) to make it feel dynamic.
- Respect prefers-reduced-motion: reduce by disabling the animation.
- Use oklch() for explicit colors and site tokens (var(--card), var(--muted), etc.) where appropriate.

Framework variant (pick one):
A) Vanilla HTML/CSS with 4 digits and an optional suffix.
B) React component <RollingCounter digits={4} /> that renders the strips.

Edge cases to handle:
- Layout shift: ensure the digit window has a fixed width and height.
- Font rendering: use a stable font and align digits center.
- Screen readers: provide an aria-label with a static value if needed.

Return HTML + CSS.

Why this matters in 2026

Not every counter needs JavaScript. For decorative, “alive” UI (loading stats, dashboard ambience), a CSS roller delivers the right feel with almost zero complexity: it’s just a transformed strip and steps().

The logic

Instead of trying to animate text content, you render all digits in a column and move the column. steps(10) converts the animation into 10 discrete jumps, so each digit snaps cleanly and never blurs between values.

Accessibility & performance

Because this is motion, always include a prefers-reduced-motion fallback. Performance-wise, animating transform is compositor-friendly. If the counter conveys important information, render the real value in text and treat the animation as decoration.