Motion patterns Get motion.css

Patterns

Odometer

Numbers roll like counter wheels. The last digit spins the most, as many turns as the change really passed, and the digits ripple from the left. Change data-value to roll again.

Open the demo alone
0.00s

Timeline loads with the demo.

Numbers that roll like counter wheels. The last digit spins the most, as many extra turns as the change really passed through (at most three), and the wheels start one after another from the left, so the number ripples into place and lands with a small settle.

Use

<link rel="stylesheet" href="tokens.css">
<link rel="stylesheet" href="odometer/style.css">
<script src="play.js"></script>
<script src="odometer/odometer.js" defer></script>

RM <span class="odometer" data-value="4416" data-decimals="2">4,416.00</span>

The first roll starts from zero when the number scrolls into view. To roll again, change the attribute:

document.querySelector(".odometer").dataset.value = 4812.5;

It rolls from the number it shows to the new one, forward when the number goes up and backward when it goes down.

Options

Attribute or propertyDoesDefault
data-valueThe number to show. Change it to rollRequired
data-decimalsDigits after the point0
--odo-hHeight of one digit1.15em
--odo-padRoom above and below each window for the fade.18em

Style the windows through .odo-wheel and the commas and points through .odo-sep. The demo gives each wheel a tinted window.

Do

  • Use it for a number that changes while someone watches: takings, tickets sold, a live count.
  • Keep the plain number in the markup. It is what shows without the script, and what screen readers read.

Do not

  • Roll a number that changes every second. Each roll takes about a second and a half.
  • Use it for long body numbers. It is for one figure per tile.

Reduced motion

The wheels go straight to the new digits. Nothing rolls.

style.css

/*
 * odometer: numbers that roll like counter wheels. odometer.js builds one wheel per digit; this
 * file shapes them. Each wheel is a window one digit tall with a strip of digits behind it. The
 * strip's edges fade, so a rolling digit blurs into the window edge instead of being cut by it.
 * Without the script, the plain number shows and nothing moves.
 */
.odometer { display: inline-flex; font-variant-numeric: tabular-nums; white-space: nowrap; }
.js .odometer[data-value]:not(.is-ready) { visibility: hidden; }
.odo-text {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}
.odo-face { display: inline-flex; align-items: flex-start; }
.odo-wheel {
  position: relative;
  display: inline-block;
  height: var(--odo-h, 1.15em);
  overflow: hidden;
  -webkit-mask-image: linear-gradient(transparent, #000 22%, #000 78%, transparent);
  mask-image: linear-gradient(transparent, #000 22%, #000 78%, transparent);
}
/* the mask needs room: pad the window and pull the neighbours back so the digit keeps its line */
.odo-wheel { padding-block: var(--odo-pad, .18em); margin-block: calc(var(--odo-pad, .18em) * -1); height: calc(var(--odo-h, 1.15em) + var(--odo-pad, .18em) * 2); }
.odo-strip { display: block; }
.odo-strip > span { display: block; height: var(--odo-h, 1.15em); line-height: var(--odo-h, 1.15em); text-align: center; }
.odo-sep { line-height: var(--odo-h, 1.15em); }

@media (prefers-reduced-motion: reduce) {
  /* odometer.js sets each wheel straight to its digit; nothing rolls */
  .odo-strip { transition: none; }
}