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).
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