Breaking down Svelte's demo counter animations
Animations, how do they work?
Introduction
If you’ve ever worked with Svelte, you’ve likely seen this:
Insert video of the counter
But how does it actually work? I recently wanted to create a similar effect and decided to dive into the code. While it looks simple on the surface, there are some unintuitive aspects to it, so let’s break it down together.
Browsing the code
Let’s start by taking a look at the Counter component. The first lines of code are this:
import { spring } from 'svelte/motion';
let count = 0;
const displayed_count = spring();
$: displayed_count.set(count);
$: offset = modulo($displayed_count, 1);
function modulo(n: number, m: number) {
// handle negative numbers
return ((n % m) + m) % m;
}
We see that there is a variable called count
. This gets incremented or decremented in the on:click
handler for the + and - buttons respectively. Makes sense so far.
Next we see that displayed_count
is a spring. A spring (like a tween) from the svelte/motion
module is a writable store with a set()
method, but here’s the trick: the value doesn’t update immediately, but gradually interpolates between its current value and the new value. That means that instead of jumping from 0 to 1, it moves through 0.001… to 0.999… before landing at 1. This is great for smoothly animating between two values, so its use here makes a lot of sense too.
This behavior is triggered by the reactive call to displayed_count.set()
when our count
variable is updated.
I’m not sure yet what the offset is used for or why it needs a modulo for negative numbers, so let’s set that aside for a second.
The next thing that catches my eye is this:
<div class="counter-viewport">
<div class="counter-digits" style="transform: translate(0, {100 * offset}%)">
<strong class="hidden" aria-hidden="true">{Math.floor($displayed_count + 1)}</strong>
<strong>{Math.floor($displayed_count)}</strong>
</div>
</div>
Here we see our offset used in a transform: translate()
, so we know it’s used to actually animate the position of the elements. This is also where it all starts making a little less sense to me.
We can see there are two <strong>
elements, one for the displayed_count
and one for displayed_count
+ 1 that has a class of hidden
. We know that our counter looks the same whether we increment or decrement, so I’m curious why we need an element for the counter + 1, but not for the counter - 1.
The only other code of note is that hidden
class, which has a top: -100%
definition in the CSS at the end of the file.
Let’s make it more visual
Now that we’ve seen the code behind it, let’s play with it a bit to see what everything does. With the offset
variable being used in the transform statement, it seems like that is actually driving the movement of the numbers, so we’ll visualize its value and the value of displayed_count
that it is based on. We also already know about the “hidden” element that has the counter + 1 value, that is being hidden above the current counter value. Let’s remove the overflow: hidden
from their parent to better see what is happening. While we’re at it, I’ll also update the spring’s parameters so the animation is slowed down a little, and I’ll remove some of the existing text to make it less cluttered. Let’s start by looking at what happens when you increment the counter.
Insert video here
As expected, we can see that the value of displayed_count
moves from the current count value towards the next and we can see that the offset is based on that. In fact, when incrementing from 0 to 1, the values for both variables are the same. When we reach higher numbers though, we can see that the offset always stays between 0 and 1. This is the work of our friend the modulo()
function and it is needed for our transform to work, since our offset is multiplied by 100 and should give a transform percentage. If we just used the displayed_count
value for this, we would get transforms of more than 100% times the value of the counter, which would look quite silly.
Another thing that becomes clear from this visualization is how the elements reset to their original position, ready for the next animation. As soon as displayed_count
reaches its new value, the value of Math.floor($displayed_count)
also becomes the new value of count
. This is significant, because it makes the values in our two strong tags “reset”. If we increment from 0 to 1, the initial strong tag only flips from 0 to 1 as soon as the animation is done and the “hidden” strong tag flips from 1 to 2. Our elements now contain the correct values for the next increment. At the same time, our offset resets from 100% back to 0%, making the elements return to their original position.
What about decrementing?
This is what intrigued me so much about this animation. I know that the offset is used to slowly make the incremented value slowly appear, but what about the decremented value? There is no element with the value of displayed_count
- 1.
Well, this is also the work of the modulo()
function. A regular modulo operator (%) would work for the increment example, but not for the decrement. The modulo outcome of a negative decimal value would also be negative and we know our offset
only works with values between 0 and 1. The function ensures that even these types of inputs lead to an outcome between 0 and 1.
Now let’s look at the visualization again:
Insert video here
Now it all makes sense. When incrementing, the elements jump to their new position at the end of the animation, but when decrementing, this happens at the very beginning. The offset jumps to 1, making the elements jump down. It then decreases towards 0, making the elements move up. At the same time, our Math.floor($displayed_count)
immediately jumps to the lower value, since it always rounds down. As soon as the spring’s value changes from 1 to 0.00009, it gets rounded to 0. This makes it so that the elements that just jumped to their new positions contain the correct value.
Let’s recreate it
To test our understanding, let’s see if we can recreate the effect ourselves.
In conclusion
While the effect seems quite simple at first, it’s actually a careful coordination of a few different values. The end result is a clean animation, with only the HTML elements that are needed. It shows some techniques, like using a spring and using it to dynamically set CSS transforms, that can be used to craft delightful animations throughout your app. And next time you generate a Svelte project with the demo, you’ll know exactly what’s going on behind that delightful counter.