Tutorials

avatar

Malik Kotb

Jan 22, 2024 / Beginner / Short

Animated Counter
Animated Counter Loading Screen Animation

A web animation tutorial on creating an animated-counter loading screen using Next.js, TailwindCSS and Framer Motion. This tutorial offers a step-by-step guide to adding this interactive element to your loading screens.

Initializing the project

Let's start the project by setting up a Next.js app with TypeScript and utilizing the app router. Just run npx create-next-app@latest client in the terminal.

Adding the JSX and CSS

We can clear out all the contents in the page.tsx, and global.css files, and add our own HTML and CSS, to begin with a clean slate in the application.

Page Component

We create a black, full-screen background and place our AnimateCounter component in the center.

page.tsx

1import AnimateCounter from "@/components/AnimatedCounter";
2
3export default function Home() {
4  return (
5    <main className="bg-black h-screen flex justify-center items-center">
6	    <AnimateCounter />
7    </main>
8  )
9}
10

AnimatedCounter Component

Create a components directory (next to the app directory) and create a new file AnimatedCounter.tsx.

Imports

page.tsx

1"use client";
2import { MotionValue, motion, useSpring, useTransform } from "framer-motion";
3import { useEffect, useState } from "react";
4

AnimateCounter Component

Initialize state count with 0 and inside a useEffect hook update count to 100 after 200 milliseconds, which will trigger our animation.

page.tsx

1export default function AnimateCounter() {
2  let [count, setCount] = useState(0);
3
4  useEffect(() => {
5    setTimeout(() => {
6      setCount(100);
7    }, 200);
8  }, []);
9
10  return (
11    <div>
12      <Counter value={count} />
13    </div>
14  );
15}
16

Counter Component

The counter component entails the main functionality of the animation.

page.tsx

1function Counter({ value }: {value:  number}) {
2  let animatedValue = useSpring(value, {
3    stiffness: 50,
4    damping: 20,
5    duration: 2,
6  });
7  useEffect(() => {
8    animatedValue.set(value);
9  }, [animatedValue, value]);
10
11  return (
12    <div className="flex h-32 text-white text-9xl font-medium overflow-hidden">
13      <div className="relative w-20">
14        {[...Array(10).keys()].map((i) => (
15          <Number place={100} mv={animatedValue} number={i} key={i} />
16        ))}
17      </div>
18      <div className="relative w-20">
19        {[...Array(10).keys()].map((i) => (
20          <Number place={10} mv={animatedValue} number={i} key={i} />
21        ))}
22      </div>
23      <div className="relative w-24">
24        {[...Array(10).keys()].map((i) => (
25          <Number place={1} mv={animatedValue} number={i} key={i} />
26        ))}
27      </div>
28    </div>
29  );
30}
31

page.tsx

1type NumberType = {
2  place: number;
3  mv: MotionValue;
4  number: number;
5};
6
7function Number({ place, mv, number }: NumberType) {
8  let y = useTransform(mv, (latest) => {
9    let height = 128; 
10    let placeValue = (latest / place) % 10;
11    let offset = (10 + number - placeValue) % 10;
12    let memo = offset * height;
13    if (offset > 5) {
14      memo -= 10 * height;
15    }
16    return memo;
17  });
18
19  return (
20    <motion.span style={{ y }} className="absolute inset-0 flex justify-center">
21      {number}
22    </motion.span>
23  );
24}
25
We should have something like this:
Wrapping up.

Quick and smooth, right?

Hope you liked the animation and learned something new!

-Malik

Things for creative devs
sent to your inbox every week