Odometer Roll

Entrance & Kinetic · Animated · pure CSS

Each letter spins into place like a mechanical odometer: a vertical reel of neighbouring glyphs rolls upward inside a fixed, overflow-clipped window and lands on the target with a small settling bounce, staggered left to right. Digits wrap through 0-9 and letters through the alphabet, framed in counter cells with drum-curved top and bottom shading for a hardware feel (per-letter markup).

OLMNOdabcdolmnomjklmebcdetqrstebcderopqr     ROPQRolmnolijkllijkl

How it works

Odometer Roll is an animated entrance & kinetic text effect rendered entirely in CSS. Each character is wrapped in its own span so it can animate independently — the HTML and JSX exports include that per-letter markup.

Controls

Odometer Roll exposes 3 dedicated controls — Reel Length, Roll Time and Cell Chrome — on top of the shared type controls (font, weight, letter-spacing and case). Open it in the generator to tune every value live, then copy the updated CSS.

CSS

/* Odometer Roll — made with TEXT-FX · https://text-fx.app
 * HTML: each character is wrapped in a <span> — see the HTML export.
 * Font: 'Syne', sans-serif (load from Google Fonts).
 */

.text-effect {
  font-family: 'Syne', sans-serif;
  font-weight: 700;
  letter-spacing: 8px;
  text-transform: none;
}

.text-effect {
  color: hsl(40 24% 90%);
  white-space: pre;
}
.text-effect .fx-win {
  position: relative;
  display: inline-block;
  overflow: hidden;
  height: 1.25em;
  line-height: 1.25em;
  vertical-align: baseline;
}
.text-effect .fx-sp {
  display: inline-block;
  height: 1.25em;
}
.text-effect .fx-sizer {
  visibility: hidden;
}
.text-effect .fx-strip {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  text-align: center;
  will-change: transform;
  animation: text-effect-odo 0.83s linear both;
  animation-delay: calc(var(--i) * 70ms);
}
.text-effect .fx-cell {
  display: block;
  height: 1.25em;
  line-height: 1.25em;
}
.text-effect .fx-win {
  padding: 0 0.1em;
  border-radius: 3px;
  background: hsl(0 0% 100% / 0.04);
}
.text-effect .fx-win::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: 3px;
  background: linear-gradient(to bottom, hsl(0 0% 0% / 0.62) 0%, transparent 34%, transparent 66%, hsl(0 0% 0% / 0.62) 100%);
  box-shadow: inset 0 0 0 1px hsl(40 20% 100% / 0.14);
}
.text-effect:hover .fx-strip {
  animation-name: text-effect-odo-r;
}

@keyframes text-effect-odo {
  0% { transform: translateY(0); animation-timing-function: cubic-bezier(0.35, 0, 0.15, 1); }
  62% { transform: translateY(-3.75em); animation-timing-function: cubic-bezier(0.3, 0, 0.2, 1); }
  76% { transform: translateY(-3.55em); animation-timing-function: ease-in-out; }
  88% { transform: translateY(-3.68em); animation-timing-function: ease-in-out; }
  100% { transform: translateY(-3.75em); }
}
@keyframes text-effect-odo-r {
  0% { transform: translateY(0); animation-timing-function: cubic-bezier(0.35, 0, 0.15, 1); }
  62% { transform: translateY(-3.75em); animation-timing-function: cubic-bezier(0.3, 0, 0.2, 1); }
  76% { transform: translateY(-3.55em); animation-timing-function: ease-in-out; }
  88% { transform: translateY(-3.68em); animation-timing-function: ease-in-out; }
  100% { transform: translateY(-3.75em); }
}

HTML

This effect needs the markup below (per-letter spans, SVG defs, or a data-text attribute).

<!-- Made with TEXT-FX · https://text-fx.app -->

<style>
.text-effect {
  font-family: 'Syne', sans-serif;
  font-weight: 700;
  letter-spacing: 8px;
  text-transform: none;
}

.text-effect {
  color: hsl(40 24% 90%);
  white-space: pre;
}
.text-effect .fx-win {
  position: relative;
  display: inline-block;
  overflow: hidden;
  height: 1.25em;
  line-height: 1.25em;
  vertical-align: baseline;
}
.text-effect .fx-sp {
  display: inline-block;
  height: 1.25em;
}
.text-effect .fx-sizer {
  visibility: hidden;
}
.text-effect .fx-strip {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  text-align: center;
  will-change: transform;
  animation: text-effect-odo 0.83s linear both;
  animation-delay: calc(var(--i) * 70ms);
}
.text-effect .fx-cell {
  display: block;
  height: 1.25em;
  line-height: 1.25em;
}
.text-effect .fx-win {
  padding: 0 0.1em;
  border-radius: 3px;
  background: hsl(0 0% 100% / 0.04);
}
.text-effect .fx-win::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: 3px;
  background: linear-gradient(to bottom, hsl(0 0% 0% / 0.62) 0%, transparent 34%, transparent 66%, hsl(0 0% 0% / 0.62) 100%);
  box-shadow: inset 0 0 0 1px hsl(40 20% 100% / 0.14);
}
.text-effect:hover .fx-strip {
  animation-name: text-effect-odo-r;
}

@keyframes text-effect-odo {
  0% { transform: translateY(0); animation-timing-function: cubic-bezier(0.35, 0, 0.15, 1); }
  62% { transform: translateY(-3.75em); animation-timing-function: cubic-bezier(0.3, 0, 0.2, 1); }
  76% { transform: translateY(-3.55em); animation-timing-function: ease-in-out; }
  88% { transform: translateY(-3.68em); animation-timing-function: ease-in-out; }
  100% { transform: translateY(-3.75em); }
}
@keyframes text-effect-odo-r {
  0% { transform: translateY(0); animation-timing-function: cubic-bezier(0.35, 0, 0.15, 1); }
  62% { transform: translateY(-3.75em); animation-timing-function: cubic-bezier(0.3, 0, 0.2, 1); }
  76% { transform: translateY(-3.55em); animation-timing-function: ease-in-out; }
  88% { transform: translateY(-3.68em); animation-timing-function: ease-in-out; }
  100% { transform: translateY(-3.75em); }
}
</style>

<div class="text-effect"><span class="fx-win" style="--i:0"><span class="fx-sizer">Y</span><span class="fx-strip"><span class="fx-cell">V</span><span class="fx-cell">W</span><span class="fx-cell">X</span><span class="fx-cell">Y</span></span></span><span class="fx-win" style="--i:1"><span class="fx-sizer">o</span><span class="fx-strip"><span class="fx-cell">l</span><span class="fx-cell">m</span><span class="fx-cell">n</span><span class="fx-cell">o</span></span></span><span class="fx-win" style="--i:2"><span class="fx-sizer">u</span><span class="fx-strip"><span class="fx-cell">r</span><span class="fx-cell">s</span><span class="fx-cell">t</span><span class="fx-cell">u</span></span></span><span class="fx-win" style="--i:3"><span class="fx-sizer">r</span><span class="fx-strip"><span class="fx-cell">o</span><span class="fx-cell">p</span><span class="fx-cell">q</span><span class="fx-cell">r</span></span></span><span class="fx-win" style="--i:4"><span class="fx-sizer"> </span><span class="fx-strip"><span class="fx-cell"> </span><span class="fx-cell"> </span><span class="fx-cell"> </span><span class="fx-cell"> </span></span></span><span class="fx-win" style="--i:5"><span class="fx-sizer">t</span><span class="fx-strip"><span class="fx-cell">q</span><span class="fx-cell">r</span><span class="fx-cell">s</span><span class="fx-cell">t</span></span></span><span class="fx-win" style="--i:6"><span class="fx-sizer">e</span><span class="fx-strip"><span class="fx-cell">b</span><span class="fx-cell">c</span><span class="fx-cell">d</span><span class="fx-cell">e</span></span></span><span class="fx-win" style="--i:7"><span class="fx-sizer">x</span><span class="fx-strip"><span class="fx-cell">u</span><span class="fx-cell">v</span><span class="fx-cell">w</span><span class="fx-cell">x</span></span></span><span class="fx-win" style="--i:8"><span class="fx-sizer">t</span><span class="fx-strip"><span class="fx-cell">q</span><span class="fx-cell">r</span><span class="fx-cell">s</span><span class="fx-cell">t</span></span></span></div>
Category
Entrance & Kinetic
Type
Animated
Browser support
All modern browsers
Capabilities
perLetter

Related Entrance & Kinetic effects