The Odometer Effect (without JavaScript)
The Odometer Effect (without JavaScript) ź“ė Ø
With CSS, we can fill numbers into HTML elements now, thanks to the attr() function and a bit of trickery. This allows design effects to be applied to those numbers. Today, weāll look at an odometer effect, meaning numbers that āspinā vertically, like the mileage meter on a vehicle. This effect is useful for dynamically displaying numeric values and drawing the userās attention when the values change, such as a rolling number of online users, a tracked price, or a timer.
The above example shows an amount upto the place value of millions. Iāll include more examples as we go.
<data id="amount" value="3284915">
<span class="digit"> <!-- Millions --> </span>
<span class="digit"> <!-- Hundred Thousands --> </span>
<span class="digit"> <!-- Ten Thousands --> </span>
<span class="digit"> <!-- Thousands --> </span>
<span class="digit"> <!-- Hundreds --> </span>
<span class="digit"> <!-- Tens --> </span>
<span class="digit"> <!-- Ones --> </span>
</data>
The amount is in the value attribute of the <data> element. You can use any other suitable element and attribute combination, like <div data-price="60589">. Iāve not included the comma separator in the HTML now; weāll get to that later.
Autofill Numbers
Letās first get the number from the HTML attribute into a CSS variable using the attr(<attr-name> <attr-type>) function.
#amount {
--amt: attr(value number);
}
Weāll also need each .digitās position, for which we use sibling-index().
#amount {
--amt: attr(value number);
.digit {
--si: sibling-index();
}
}
Now, we fill each .digitās pseudo-elements with each digit from the number. To extract the digits from the number one by one, we use the mod() function.
.amt {
--amt: attr(value number);
.digit {
--si: sibling-index();
/* autofill digits */
&::after {
/* Divide the number by the power of 10, round down,
and use mod() to isolate a single integer (0-9) */
counter-set: n mod(round(down,var(--amt)/(10000/pow(10,var(--si)-1))),10);
content: counter(n);
}
}
}
Note
The CSS mod() function returns the remainder of a division.
To make it easier to demonstrate, hereās an example of autofilling digits for a three-digit number:
<data id="weight" value="420">
<span class="digit"></span>
<span class="digit"></span>
<span class="digit"></span>
gms
</data>
#weight {
--wgt: attr(value number);
.digit {
--si: sibling-index();
&::after {
counter-set: n mod(round(down,var(--wgt)/(100/pow(10,var(--si)-1))),10);
content: counter(n);
}
}
}
Hereās how the math works:
sibling-index() = 1
mod(round(down, 420/(100/pow(10,1-1))), 10)
mod(round(down, 420/(100/pow(10, 0))), 10)
mod(round(down, 420/(100/1)), 10)
mod(round(down, 420/100), 10)
mod(round(down, 4.2), 10)
mod(4, 10)
**= 4
**
sibling-index() = 2
mod(round(down, 420/(100/pow(10,2-1))), 10)
mod(round(down, 420/(100/pow(10, 1))), 10)
mod(round(down, 420/(100/10)), 10)
mod(round(down, 420/10), 10)
mod(round(down, 42), 10)
mod(42, 10)
**= 2
**
sibling-index() = 3
mod(round(down, 420/(100/pow(10,3-1))), 10)
mod(round(down, 420/(100/pow(10, 2))), 10)
mod(round(down, 420/(100/100)), 10)
mod(round(down, 420/1), 10)
mod(round(down, 420), 10)
mod(420, 10)
**= 0**
Adding Separators
When we add a separator character in the mix, using sibling-index() alone wonāt give the right position of the digits following the separator. We have to exclude the separators from the math. Hereās an example:
<data id="amount" value="7459328">
<span class="digit"></span>
<span class="digit"></span>
<span class="separator">,</span>
<span class="digit"></span>
<span class="digit"></span>
<span class="separator">,</span>
<span class="digit"></span>
<span class="digit"></span>
<span class="digit"></span>
KRW
</data>
.digit {
--si: sibling-index();
&::after {
counter-set: n mod(round(down,var(--amt)/(1000000/pow(10,var(--i)))),10);
content: counter(n);
}
/* first two digits */
&:nth-child(-n+2)::after {
--i: var(--si) - 1;
}
/* third and fourth digits */
&:where(:nth-child(3 of .digit),:nth-child(4 of .digit))::after {
--i: var(--si) - 2;
}
/* last three digits */
&:nth-last-child(-n+3)::after {
--i: var(--si) - 3;
}
}
For each separator break, decrement the sibling index by 1 for the following digits.
The Animation
Now that the digits can be automatically separated into distinct elements, we can apply any animation we want to them individually. For the odometer effect, Iām adding animations that slide the digits up and down as the count decreases, mimicking the rolling style.
@property --n {
syntax: "<integer>";
inherits: false;
initial-value: 0;
}
@keyframes count {
from { --n: 9; }
to { --n: 0; }
}
@keyframes slideDown {
from { transform: translateY(-100%); }
to { transform: translateY(100%); }
}
@keyframes slideUp {
from { transform: translateY(100%); }
to { transform: translateY(-100%); }
}
The --n variable, of integer type, is animated in the @keyframes animation count, decrementing from 9 to 0.
&::after {
/* Save the digit in a variable */
--digit: mod(round(down, var(--amt) / (1000000/pow(10, var(--i)))), 10);
/* Show whichever is higher: the active countdown value (--n) or the target digit. Prevents the counter from dropping below the final value. */
counter-set: n max(var(--n), var(--digit));
content: counter(n);
/* The 1s is the countdown animation.
The 0.11s (1/9) slide animation repeats until countdown hits the target digit. */
animation: linear 1s, linear 0.11s calc(9 - var(--digit)) ;
}
&:nth-of-type(even)::after {
animation-name: count, slideUp;
}
&:nth-of-type(odd)::after {
animation-name: count, slideDown;
}
The demo from before:
Varying Speed and Style
Since the animation uses repeated vertical displacement to create the rolling effect, to speed up, pause, or slow down the digits, either by count or position (sibling index), set any animationās time, delay, or repetition based on the count, position, or both.
Hereās an example where the later counts are slightly slower:
&:after {
animation:
1.4s linear,
0.11s linear calc(5 - var(--digit)),
0.22s linear 0.55s calc(4 - var(--digit));
}
&:nth-of-type(even)::after {
animation-name: count, slideUp, slideUp;
}
&:nth-of-type(odd)::after {
animation-name: count, slideDown, slideDown;
}
Hereās one where thereās no count or rolling, just a jittery effect.
animation: 0.1s linear calc(0.1s * var(--si));
Although this post covered the odometer effect, its concept can be applied to other graphic effects involving numbers. Being able to autofill numbers into individual elements, and compute and animate them, all in CSS, simplifies designing visual changes for dynamic numeric values on screen.