How to Make an Animated Number Counter with Tailwind CSS
Tags: webdev, tailwindcss, html
Published: 2023-10-03 07:55:12

####**[Live Demo](https://cruip-tutorials.vercel.app/animated-number-counter/) / [Download](https://github.com/cruip/cruip-tutorials/blob/main/animated-number-counter/index.html)**

An **animated counter** is a particular type of effect applied to numeric elements to give them dynamism.

From a UX perspective, this effect is recommended for drawing the user’s attention to important data or statistics, while It’s not advised if the information is not particularly relevant as the animation requires some time to load.

At the moment we are writing this article, we have never included this element in any of our [Tailwind CSS templates](https://cruip.com/?swcfpc=1), so we thought it would be helpful to write a tutorial that explains how to create it and add it to your landing page or website (alternatively you can use the ready-made one we’ve coded for you in this tutorial – it’s free!).

Let’s get started!

Explaining the technique
------------------------

Did you know that you can create a an **animated number counter using only CSS**? Well, you can! In this tutorial, we will show you how to do it with Tailwind CSS.

Before diving into the code, let’s explain the technique we’ll use to create this animation. Some time ago, creating such an animation required JavaScript. However, thanks to the `@property` CSS at-rule, we can now animate CSS variables. This marks a significant evolution in CSS because it allows us not only to animate numbers but also to animate letters!

In a nutshell, what we’ll do is define a CSS `@property` representing our number, with an `initial-value`. This property will be animated using a simple CSS `transition`. Finally, we’ll use the `counter-set` property to display the counter value.

**Important:** Before we proceed, it’s important to note that, as of our writing, this technique only works in Chrome and Chromium-based browsers like Edge and Opera. While the `@property` at-rule has been supported by major browsers for some time, Safari does not support the `counter-set` property yet, and Firefox may have issues with transitioning from the initial value to the target number. However, you can confidently use this approach in production, as it gracefully degrades to show the value without animation in non-supporting browsers.

Creating the HTML structure
---------------------------

OK, let’s create a section with a grid layout made of 3 blocks. Each block contains an icon, a number, a title, and a description. First, add the `grid` class to the section and the `grid-cols-3` class to define the 3-column layout. Also, add the `gap-12` class to set the spacing between columns.

```html
<section class="grid gap-12 md:grid-cols-3 md:gap-16">
    
    <!-- Block #1 -->
    <article>
        <div class="w-14 h-14 rounded shadow-md bg-white flex justify-center items-center rotate-3 mb-6">
            <svg xmlns="http://www.w3.org/2000/svg" width="31" height="20">
                <defs>
                    <linearGradient id="icon1-a" x1="50%" x2="50%" y1="0%" y2="100%">
                        <stop offset="0%" stop-color="#A5B4FC" />
                        <stop offset="100%" stop-color="#4F46E5" />
                    </linearGradient>
                    <linearGradient id="icon1-b" x1="50%" x2="50%" y1="0%" y2="100%">
                        <stop offset="0%" stop-color="#EEF2FF" />
                        <stop offset="100%" stop-color="#C7D2FE" />
                    </linearGradient>
                </defs>
                <g fill="none" fill-rule="nonzero">
                    <path fill="url(#icon1-a)" d="M20.625 0H9.375a9.375 9.375 0 0 0 0 18.75h11.25a9.375 9.375 0 0 0 0-18.75Z" transform="translate(.885 .885)" />
                    <path fill="url(#icon1-b)" d="M9.375 17.5A8.125 8.125 0 0 1 1.25 9.375 8.125 8.125 0 0 1 9.375 1.25 8.125 8.125 0 0 1 17.5 9.375 8.125 8.125 0 0 1 9.375 17.5Z" transform="translate(.885 .885)" />
                </g>
            </svg>
        </div>
        <h2>
            <span class="flex text-slate-900 text-5xl font-extrabold mb-2">
                40K+
            </span>
            <span class="inline-flex font-semibold bg-clip-text text-transparent bg-gradient-to-r from-indigo-500 to-indigo-300 mb-2">Variations</span>
        </h2>
        <p class="text-sm text-slate-500">Many desktop publishing packages and web page editors now use Pinky as their default model text.</p>
    </article>

    <!-- Block #2 -->
    <!-- Block #3 -->

</section>
```

There isn’t much to explain. The code above defines the structure of a single block. But the number is still static, so let’s see how to animate it!

Defining the counter animation with CSS
---------------------------------------

As mentioned earlier, we’ll use the `@property` CSS property to animate the number. Ideally, you would define this property in a separate CSS file, but for simplicity, we’ll define it directly in our HTML file. So let’s add the following code inside a `<style>` tag:

```html
<style>
    @property --num {
        syntax: '<integer>';
        initial-value: 0;
        inherits: false;
    }
</style>
```

As you can see, we’ve defined a property called `--num` to represent our number. We’ve specified that the initial value is `0`, and the value can only be an `integer`. Finally, we’ve specified that this property cannot be inherited.

Now, to animate the number, we need to define a transition. At this stage, since we want to trigger the animation on page load, we must define an animation with `@keyframes`. Add the following code to our `<style>` tag and modify our inline style as follows:

```html
<style>
    @property --num {
        syntax: '<integer>';
        initial-value: 0;
        inherits: false;
    }
    @keyframes counter {
        from {
            --num: 0;
        }
        to {
            --num: 40;
        }
    }    
</style>
```

We’ve defined an animation called `counter` that goes from 0 to 40. Now, we need to modify the HTML part containing the number so that the animation works. Chabge our inline style as follows:

```html
<h2>
    <span class="flex tabular-nums text-slate-900 text-5xl font-extrabold mb-2 animate-[counter_3s_ease-out_forwards] [counter-set:_num_var(--num)] before:content-[counter(num)]">
        <span class="sr-only">40</span>K+
    </span>
    <span class="inline-flex font-semibold bg-clip-text text-transparent bg-gradient-to-r from-indigo-500 to-indigo-300 mb-2">Variations</span>
</h2>
```

Let’s go through the classes we’ve added:

*   `animate-[counter_3s_ease-out_forwards]`: This class defines the animation name, duration, and easing. It’s generated on-the-fly thanks to Tailwind CSS’s [arbitrary variants](https://tailwindcss.com/docs/adding-custom-styles#arbitrary-variants).
*   `[counter-set:_num_var(--num)]`: This class defines the counter value, which is determined by the `--num` property we defined earlier.
*   `before:content-[counter(num)]`: This class allows us to display the number as content of the `::before` pseudo-element. To use this technique, we wrap the number in a `<span>` and hide it from view using the `sr-only` class.
*   Lastly, we’ve added the `tabular-nums` class, which ensures that each number takes up uniform space. This CSS property is essential to maintain the block’s width consistency during the animation, resulting in a smoother visual experience.

The animation now works like a charm. However, starting the animation as soon as the page loads might not be ideal, especially if the counter is positioned at the bottom of the page. In that case, the animation would start immediately, potentially going unnoticed by the user.

Ideally, we’d want the animation to start only when the user reaches the block containing the counter. To achieve this, we’ll need a bit of JavaScript.

Trigger the animation with JavaScript
-------------------------------------

If you’ve followed our other [tutorials](https://cruip.com/?swcfpc=1), you might be familiar with [Alpine.js](https://alpinejs.dev/). It’s a lightweight JavaScript library that allows you to add interactivity to your site without writing a single line of JavaScript. It’s incredibly easy to use, and we’ll show you how to make the animation trigger when the user scrolls to it.

Firstly, let’s add the library to our project. Insert the following code inside the `<head>` tag:

```ts
    <script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/intersect@3.x.x/dist/cdn.min.js"></script>
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
```

In addition to Alpine.js, we’ve also imported the `@alpinejs/intersect` library, which allows us to trigger an action when an element enters or exits the viewport. This is exactly what we need to trigger the counter’s animation.

The next step is to define a variable called `shown`, initially set to `false`. We’ll change this value to `true` when the element enters the viewport. Finally, we’ll update the class of the block containing the counter based on the value of the `shown` variable. Update our HTML as follows:

```html
<h2>
    <span class="flex tabular-nums text-slate-900 text-5xl font-extrabold mb-2 transition-[_--num] duration-[3s] ease-out [counter-set:_num_var(--num)] supports-[counter-set]:before:content-[counter(num)]" x-data="{ shown: false }" x-intersect="shown = true" :class="shown && '[--num:40]'">
        <span class="supports-[counter-set]:sr-only">40</span>K+
    </span>
    <span class="inline-flex font-semibold bg-clip-text text-transparent bg-gradient-to-r from-indigo-500 to-indigo-300 mb-2">Variations</span>
</h2>
```

With this done, we can remove the `@keyframes` at-rule since it’s the `[--num:40]` class that now defines the final value of the counter.

Note that we’ve also added the `supports-[counter-set]` class to the block containing the counter. This class allows us to display the number as content of the `::before` pseudo-element only if the browser supports the `counter-set` property. This way, if the browser doesn’t support the property, the number will still be displayed, albeit without animation.

And there you have the final code:

```html
<-- Inline style -->
<style>
    @property --num {
        syntax: '<integer>';
        initial-value: 0;
        inherits: false;
    }
</style>

<-- Counters -->
<section class="grid gap-12 md:grid-cols-3 md:gap-16">
    <!-- Block #1 -->
    <article>
        <div class="w-14 h-14 rounded shadow-md bg-white flex justify-center items-center rotate-3 mb-6">
            <svg xmlns="http://www.w3.org/2000/svg" width="31" height="20">
                <defs>
                    <linearGradient id="icon1-a" x1="50%" x2="50%" y1="0%" y2="100%">
                        <stop offset="0%" stop-color="#A5B4FC" />
                        <stop offset="100%" stop-color="#4F46E5" />
                    </linearGradient>
                    <linearGradient id="icon1-b" x1="50%" x2="50%" y1="0%" y2="100%">
                        <stop offset="0%" stop-color="#EEF2FF" />
                        <stop offset="100%" stop-color="#C7D2FE" />
                    </linearGradient>
                </defs>
                <g fill="none" fill-rule="nonzero">
                    <path fill="url(#icon1-a)" d="M20.625 0H9.375a9.375 9.375 0 0 0 0 18.75h11.25a9.375 9.375 0 0 0 0-18.75Z" transform="translate(.885 .885)" />
                    <path fill="url(#icon1-b)" d="M9.375 17.5A8.125 8.125 0 0 1 1.25 9.375 8.125 8.125 0 0 1 9.375 1.25 8.125 8.125 0 0 1 17.5 9.375 8.125 8.125 0 0 1 9.375 17.5Z" transform="translate(.885 .885)" />
                </g>
            </svg>
        </div>
        <h2>
            <span class="flex tabular-nums text-slate-900 text-5xl font-extrabold mb-2 transition-[_--num] duration-[3s] ease-out [counter-set:_num_var(--num)] supports-[counter-set]:before:content-[counter(num)]" x-data="{ shown: false }" x-intersect="shown = true" :class="shown && '[--num:40]'">
                <span class="supports-[counter-set]:sr-only">40</span>K+
            </span>
            <span class="inline-flex font-semibold bg-clip-text text-transparent bg-gradient-to-r from-indigo-500 to-indigo-300 mb-2">Variations</span>
        </h2>
        <p class="text-sm text-slate-500">Many desktop publishing packages and web page editors now use Pinky as their default model text.</p>
    </article>
    <!-- Block #2 -->
    <article>
        <div class="w-14 h-14 rounded shadow-md bg-white flex justify-center items-center -rotate-3 mb-6">
            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="19">
                <defs>
                    <linearGradient id="icon2-a" x1="50%" x2="50%" y1="0%" y2="100%">
                        <stop offset="0%" stop-color="#A5B4FC" />
                        <stop offset="100%" stop-color="#4F46E5" />
                    </linearGradient>
                    <linearGradient id="icon2-b" x1="50%" x2="50%" y1="0%" y2="100%">
                        <stop offset="0%" stop-color="#E0E7FF" />
                        <stop offset="100%" stop-color="#A5B4FC" />
                    </linearGradient>
                </defs>
                <g fill="none" fill-rule="nonzero">
                    <path fill="url(#icon2-a)" d="M5.5 0a5.5 5.5 0 0 0 0 11c.159 0 .314-.01.469-.024a15.896 15.896 0 0 1-2.393 6.759A.5.5 0 0 0 4 18.5h1a.5.5 0 0 0 .362-.155C7.934 15.64 11 11.215 11 5.5A5.506 5.506 0 0 0 5.5 0Z" />
                    <path fill="url(#icon2-b)" d="M18.5 0a5.5 5.5 0 0 0 0 11c.159 0 .314-.01.469-.024a15.896 15.896 0 0 1-2.393 6.759.5.5 0 0 0 .424.765h1a.5.5 0 0 0 .363-.155C20.934 15.64 24 11.215 24 5.5A5.506 5.506 0 0 0 18.5 0Z" />
                </g>
            </svg>
        </div>
        <h2>
            <span class="flex tabular-nums text-slate-900 text-5xl font-extrabold mb-2 transition-[_--num] duration-[3s] ease-out [counter-set:_num_var(--num)] supports-[counter-set]:before:content-[counter(num)]" x-data="{ shown: false }" x-intersect="shown = true" :class="shown && '[--num:70]'">
                <span class="supports-[counter-set]:sr-only">70</span>K+
            </span>
            <span class="inline-flex font-semibold bg-clip-text text-transparent bg-gradient-to-r from-indigo-500 to-indigo-300 mb-2">Lessons</span>
        </h2>
        <p class="text-sm text-slate-500">Many desktop publishing packages and web page editors now use Pinky as their default model text.</p>
    </article>
    <!-- Block #3 -->
    <article>
        <div class="w-14 h-14 rounded shadow-md bg-white flex justify-center items-center rotate-3 mb-6">
            <svg xmlns="http://www.w3.org/2000/svg" width="26" height="26">
                <defs>
                    <radialGradient id="icon3-a" cx="68.15%" cy="27.232%" r="67.641%" fx="68.15%" fy="27.232%">
                        <stop offset="0%" stop-color="#E0E7FF" />
                        <stop offset="100%" stop-color="#A5B4FC" />
                    </radialGradient>
                </defs>
                <g fill="none" fill-rule="nonzero">
                    <circle cx="13" cy="13" r="13" fill="url(#icon3-a)" />
                    <path fill="#4F46E5" fill-opacity=".56" d="M0 13a12.966 12.966 0 0 0 4.39 9.737l1.15-1.722s.82-.237.997-.555c.554-.997-.43-2.733-.43-2.733a5.637 5.637 0 0 0-.198-1.23c-.148-.369-1.182-.874-1.182-.874S3.73 13.998 3.73 13a1.487 1.487 0 0 1 1.404-1.55 2.424 2.424 0 0 0 1.588-1.146s1.256-.332 1.551-.847c.295-.515-.332-2.36-.332-2.36a3.086 3.086 0 0 0-.012-1.481 2.8 2.8 0 0 0-.93-1.12 6.143 6.143 0 0 0-1.447-2.148A12.981 12.981 0 0 0 0 13ZM13 0c-.35 0-.696.018-1.04.045-.112.35-.695 1.248-.548 1.653.147.406 1.353.783 1.353.783s-.32 1.25.235 1.692c.554.443 1.44-.148 1.773-.037.331.111.258 2.29.258 2.29s1.07 1.181 2.124 1.33c1.053.147 2.656-1.64 2.656-1.64a21.131 21.131 0 0 0 3.448-1.102A12.974 12.974 0 0 0 13 0Z" />
                    <path fill="#6366F1" fill-opacity=".4" d="M21.398 13.848c.296.702-.555 2.494-1.256 2.843a4.76 4.76 0 0 0-1.82 1.452c-.259.406-.598 2.082-1.447 2.415-.85.332-2.863 2.228-3.934 1.932-1.071-.296-1.071-2.842-.333-3.988.441-.683-.074-2.179-.113-2.695-.039-.517-1.586-1.478-1.586-1.994 0-.813 1.772-2.955 1.772-2.955s1.453-.48 1.896-.37c.448.164.877.374 1.28.628.782.058 1.552.22 2.29.48l.848.775s2.107.777 2.403 1.477Z" />
                </g>
            </svg>

        </div>
        <h2>
            <span class="flex tabular-nums text-slate-900 text-5xl font-extrabold mb-2 transition-[_--num] duration-[3s] ease-out [counter-set:_num_var(--num)] supports-[counter-set]:before:content-[counter(num)]" x-data="{ shown: false }" x-intersect="shown = true" :class="shown && '[--num:149]'">
                <span class="supports-[counter-set]:sr-only">149</span>+
            </span>
            <span class="inline-flex font-semibold bg-clip-text text-transparent bg-gradient-to-r from-indigo-500 to-indigo-300 mb-2">Workshops</span>
        </h2>
        <p class="text-sm text-slate-500">Many desktop publishing packages and web page editors now use Pinky as their default model text.</p>
    </article>
</section>
```

Conclusions
-----------

In this tutorial, we have taken an unconventional approach by using only CSS to create the counter animation. If achieving consistent results across all browsers is not a priority for you, this approach provides a more interesting alternative for this kind of stuff.

If you liked this tutorial, check out our entire series of [Tailwind CSS tutorials](https://cruip.com/tutorials/?swcfpc=1) or our [ready-made templates](https://cruip.com/?swcfpc=1) if you’re looking for a pre-built interface for your next project.