April 8, 2026SimpleLoaders & Intros
Counter Loading Screen
A numeric counter ticks from 0 to 100 during a loading screen using GSAP's object tweening, then the screen wipes away with a yPercent animation to reveal the page beneath.
Preview
0
Source
"use client";
import { useEffect, useRef, useState } from "react";
import gsap from "gsap";
import styles from "./styles.module.css";
export default function CounterLoadingScreen({ duration = 2500 }) {
const [count, setCount] = useState(0);
const [done, setDone] = useState(false);
const [hidden, setHidden] = useState(false);
const screenRef = useRef(null);
const countRef = useRef({ value: 0 });
useEffect(() => {
const obj = countRef.current;
gsap.to(obj, {
value: 100,
duration: duration / 1000,
ease: "power1.inOut",
onUpdate() {
setCount(Math.floor(obj.value));
},
onComplete() {
setDone(true);
// Wipe the screen away
gsap.to(screenRef.current, {
yPercent: -100,
duration: 0.7,
ease: "power3.inOut",
delay: 0.1,
onComplete: () => setHidden(true),
});
},
});
}, [duration]);
if (hidden) {
return (
<div className={styles.revealed}>
<p className={styles.revealedText}>Page revealed</p>
<button
className={styles.resetBtn}
onClick={() => {
setCount(0);
setDone(false);
setHidden(false);
countRef.current.value = 0;
const obj = countRef.current;
gsap.to(obj, {
value: 100,
duration: duration / 1000,
ease: "power1.inOut",
onUpdate() { setCount(Math.floor(obj.value)); },
onComplete() {
setDone(true);
gsap.to(screenRef.current, {
yPercent: -100,
duration: 0.7,
ease: "power3.inOut",
delay: 0.1,
onComplete: () => setHidden(true),
});
},
});
}}
>
Replay
</button>
</div>
);
}
return (
<div className={styles.wrapper}>
<div ref={screenRef} className={styles.screen}>
<span className={styles.counter}>{count}</span>
<div
className={styles.progress}
style={{ transform: `scaleX(${count / 100})` }}
/>
</div>
</div>
);
}
Dependencies
gsap