Number animation, as in, imagine a number changing from 1 to 2, then 2 to 3, then 3 to 4, etc. over a specified time. Like a counter, except controlled by the same kind of animation that we use for other design animation on the web. This could be useful when designing something like a dashboard, to bring a little pizazz to the numbers. Amazingly, this can now be done in CSS without much trickery. You can jump right to the new solution if you like, but first let’s look at how we used to do it.
One fairly logical way to do number animation is by changing the number in JavaScript. We could do a rather simple setInterval
, but here’s a fancier answer with a function that accepts a start, end, and duration, so you can treat it more like an animation:
Keeping it to CSS, we could use CSS counters to animate a number by adjusting the count at different keyframes:
Another way would be to line up all the numbers in a row and animate the position of them only showing one at a time:
Some of the repetitive code in these examples could use a preprocessor like Pug for HTML or SCSS for CSS that offer loops to make them perhaps easier to manage, but use vanilla on purpose so you can see the fundamental ideas.
The New School CSS Solution
With recent support for CSS.registerProperty
and @property
, we can animate CSS variables. The trick is to declare the CSS custom property as an integer; that way it can be interpolated (like within a transition) just like any other integer.
@property --num {
syntax: '<integer>';
initial-value: 0;
inherits: false;
}
div {
transition: --num 1s;
counter-reset: num var(--num);
}
div:hover {
--num: 10000;
}
div::after {
content: counter(num);
}
Important Note: At the time of this writing, this @property
syntax is only supported in Chrome ( and other Chromium core browsers like Edge and Opera), so this isn’t cross-browser friendly. If you’re building something Chrome-only (e.g. an Electron app) it’s useful right away, if not, wait. The demos from above are more widely supported.
The CSS content
property can be used to display the number, but we still need to use counter
to convert the number to a string because content
can only output <string>
values.
See how we can ease the animations just like any other animation? Super cool!
Typed CSS variables can also be used in @keyframes
:
One downside? Counters only support integers. That means decimals and fractions are out of the question. We’d have to display the integer part and fractional part separately somehow.
Can we animate decimals?
It’s possible to convert a decimal (e.g. --number
) to an integer. Here’s how it works:
- Register an
<integer>
CSS variable ( e.g.--integer
), with theinitial-value
specified - Then use
calc()
to round the value:--integer: calc(var(--number))
In this case, --number
will be rounded to the nearest integer and store the result into --integer
.
@property --integer {
syntax: "<integer>";
initial-value: 0;
inherits: false;
}
--number: 1234.5678;
--integer: calc(var(--number)); /* 1235 */
Sometimes we just need the integer part. There is a tricky way to do it: --integer: max(var(--number) - 0.5, 0)
. This works for positive numbers. calc()
isn’t even required this way.
/* @property --integer */
--number: 1234.5678;
--integer: max(var(--number) - 0.5, 0); /* 1234 */
We can extract the fractional part in a similar way, then convert it into string with counter (but remember that content
values must be strings). To display concatenated strings, use following syntax:
content: "string1" var(--string2) counter(--integer) ...
Here’s a full example that animates percentages with decimals:
Other tips
Because we’re using CSS counters, the format of those counters can be in other formats besides numbers. Here’s an example of animating the letters “CSS” to “YES”!
Oh and here’s another tip: we can debug the values grabbing the computed value of the custom property with JavaScript:
getComputedStyle(element).getPropertyValue('--variable')
That’s it! It’s amazing what CSS can do these days.
This is so clever! It seems like custom properties have so many secret powers the community is slowly uncovering.
Thanks ! ♥
Very clever, thanks. I’d just like to add some accessibility considerations to these examples:
These don’t work for keyboard users, but it’s easy enough to add (
tabindex="0"
on the<div>
to make it focusable, and adddiv:hover, div:focus {
in the selectors to apply the animation on focus).Screen readers read either the initial, pre-animated value (0) when navigating. When focusing or on the keyframes example, it seems to take some value while it’s animating and memorise it (for example with VoiceOver it kept telling me “8” or “61”, even though visually it was remaining at 100).
I’m not sure if there’s an elegant way of working around this — apart from a hacky way with two divs.
One upside of this approach though: unlike the JS version, this could make use of the
prefers-reduced-motion
media query and present the final value without any animation for people with vestibular/motion disorders.Thanks for the reply.
That’s the way transition works. My suggestion is also
That should be very easy to implement
I wanted to animate this one time so I’ve putted animation iteration count value 1 but the problem is it go back to zero , how can I make it stop at last value which is at 100% ?
I am talking about this one https://codepen.io/chriscoyier/pen/NWNJpPe .
Thanks.???
animation-fill-mode: forwards;
really amazing!! but wanna do it more than on time.
when i duplicate div 4 times and give each of them id to play the animation
it doesn’t play well just one or two of them work or they work but with the same value and i have 4 different values , so how to animate 4 different values ?
Have a look at my edit : https://codepen.io/bgbos/pen/pobZvwJ
i just even tried to play with -num as you will see to make it different .
Appreciate your help .
You had
--num
and@keyframes counter
being defined twice. You must fix these issues and the bad coding style before addressing your problem.ok so we have this code here : https://codepen.io/bgbos/pen/pobZvwJ
as you see each of them have a different initial value which must be stopped at but its all stopping at one value i don’t know why , so how to do it plz help?
Sorry for bad coding sir i am completely beginner.
Regards.
Is there a way to have multiple counters on a page?
can custom property be used in styled-components in react?
hello, Carter
thanks for sharing. it’s a very awesome way to achieved the effect.
I practice your solution in my project.
but it seems not working on mobile device and safari.
is it there have anyways to solve the tricky issue.
Only Chrome supports it currently and it’s not polyfill-able.
So no.
I’ve made a tiny script just for that, back in 2014:
https://github.com/yairEO/Do-in
How do I stop the counters from going back down or resetting? I want the counter to stop on the final value.
animation-fill-mode: forwards;
You need to set the animation fill mode to “forwards” then it will stay on the final value:
animation-fill-mode: forwards;
This is wonderful, but I have a strange question. If I want to use this to show a scoreboard. I would like the numbers to not reset to 0. Is there a way to keep this from happening?
Isn’t the number set by yourself?
Noticed a strange behavior when large numbers are used – the counter is always set to 0 and then jumps to the final value at the end of the animation. The length of the animation doesn’t make a difference.
div::after {
content: counter(num);
}
div:hover {
–num: 11000000;
}
Does anyone have any ideas what may cause this?
Same issue here, but mine doesn’t even jump and just stays at 0. I was pulling my hair out trying to understand why 2 of my 4 counters weren’t working despite the code being identical, until I understood it was due to the values themselves. It seems that as soon as you go over 8 digits, this stops working.
Has anyone managed to figure out why this happens, and if there’s perhaps a solution?
Filed an issue here: https://bugs.chromium.org/p/chromium/issues/detail?id=1217506
Let’s wait for some responses.
EDIT: The issue was assigned with Priority 2
EDIT2: It will be fixed in Chrome 93
https://bugs.chromium.org/p/chromium/issues/detail?id=1217506#c6
Hi! I added animation-fill-mode: forwards;
But I can’t make it stop at the end. Can anybody help me?
My code:
animation-iteration-count: 1;
Can this be tied to scroll position? For example, can I have the number counter increase as the user scrolls down and decrease as the user scrolls back up?
You need
IntersectionObserver
I created a version with IntersectionObserver that includes a few fallbacks for older browsers and prefers-reduced-motion.
Further explanation at https://www.bronco.co.uk/our-ideas/experimenting-with-css-number-counters-with-fallback/