Number Loader in 3 Steps
GSAP-powered percentage loader that animates a vertical progress bar and a slot-machine-style number counter through three randomised intermediate values before reaching 100%. Clean, minimal aesthetic with a large typographic counter.
gsaploadercounterprogressnumberanimation
Setup — External Scripts
CDN — GSAP (add before </body>)
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>Code
HTML
html
<div class="loading-container">
<div class="loading-screen">
<!-- Vertical progress bar (left edge) -->
<div class="loading__progress">
<div class="loading__progress-inner"></div>
</div>
<!-- Slot-machine number counter -->
<div class="loading__numbers">
<!-- Hundreds digit (shows "1" only at 100%) -->
<div class="loading__number-group is--first">
<div class="loading__number-wrap">
<span class="loading__number">1</span>
</div>
</div>
<!-- Tens digit (0–9 strip) -->
<div class="loading__number-group is--second">
<div class="loading__number-wrap">
<span class="loading__number">1</span>
<span class="loading__number">2</span>
<span class="loading__number">3</span>
<span class="loading__number">4</span>
<span class="loading__number">5</span>
<span class="loading__number">6</span>
<span class="loading__number">7</span>
<span class="loading__number">8</span>
<span class="loading__number">9</span>
<span class="loading__number">0</span>
</div>
</div>
<!-- Units digit (0–9 strip) -->
<div class="loading__number-group is--third">
<div class="loading__number-wrap">
<span class="loading__number">1</span>
<span class="loading__number">2</span>
<span class="loading__number">3</span>
<span class="loading__number">4</span>
<span class="loading__number">5</span>
<span class="loading__number">6</span>
<span class="loading__number">7</span>
<span class="loading__number">8</span>
<span class="loading__number">9</span>
<span class="loading__number">0</span>
</div>
</div>
<!-- Percentage symbol -->
<div class="loading__percentage-wrap">
<span class="loading__percentage">%</span>
</div>
</div>
</div>
</div>CSS
css
.loading-container {
z-index: 200;
pointer-events: none;
background-color: #E2E1DF;
position: fixed;
inset: 0;
overflow: hidden;
}
.loading-screen {
pointer-events: auto;
background-color: #E2E1DF;
width: 100%;
height: 100%;
display: none;
position: absolute;
top: 0;
left: 0;
}
.loading__progress {
width: 1em;
height: 100%;
position: absolute;
bottom: 0;
left: 0;
}
.loading__progress-inner {
transform-origin: bottom;
background-color: #ff4c24;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.loading__numbers {
flex-flow: row;
align-items: flex-start;
font-size: calc(10vw + 10vh);
display: flex;
position: absolute;
bottom: .1em;
left: .23em;
}
.loading__number-group {
flex-flow: column;
height: 1em;
display: flex;
position: relative;
overflow: hidden;
}
.loading__number-wrap {
will-change: transform;
flex-flow: column;
display: flex;
position: relative;
}
.loading__number {
text-transform: uppercase;
font-weight: 700;
line-height: 1;
position: relative;
}
.loading__percentage-wrap {
flex-flow: column;
justify-content: flex-start;
margin-top: .375em;
font-size: .3em;
display: flex;
overflow: hidden;
}
.loading__percentage {
text-transform: uppercase;
will-change: transform;
font-weight: 700;
line-height: 1;
position: relative;
}JavaScript
javascript
function initLoaderThreeSteps() {
var tl = gsap.timeline();
gsap.defaults({ ease: "Expo.easeInOut", duration: 1.2 });
// Random intermediate values for tens and units digits
var randomNumbers1 = gsap.utils.random([2, 3, 4]);
var randomNumbers2 = gsap.utils.random([5, 6]);
var randomNumbers3 = gsap.utils.random([1, 5]);
var randomNumbers4 = gsap.utils.random([7, 8, 9]);
// Show loader
tl.set(".loading-screen", { display: "block" });
tl.set(".loading__progress-inner", { scaleY: 0 });
tl.set(
".loading__number-group.is--first .loading__number-wrap, .loading__percentage",
{ yPercent: 100 }
);
tl.set(
".loading__number-group.is--second .loading__number-wrap, .loading__number-group.is--third .loading__number-wrap",
{ yPercent: 10 }
);
// Step 1 — random value (e.g. 31%)
tl.to(".loading__progress-inner", {
scaleY: (randomNumbers1 + "" + randomNumbers3) / 100,
});
tl.to(".loading__percentage", { yPercent: 0 }, "<");
tl.to(".loading__number-group.is--second .loading__number-wrap", {
yPercent: (randomNumbers1 - 1) * -10,
}, "<");
tl.to(".loading__number-group.is--third .loading__number-wrap", {
yPercent: (randomNumbers3 - 1) * -10,
}, "<");
// Step 2 — random value (e.g. 67%)
tl.to(".loading__progress-inner", {
scaleY: (randomNumbers2 + "" + randomNumbers4) / 100,
});
tl.to(".loading__number-group.is--second .loading__number-wrap", {
yPercent: (randomNumbers2 - 1) * -10,
}, "<");
tl.to(".loading__number-group.is--third .loading__number-wrap", {
yPercent: (randomNumbers4 - 1) * -10,
}, "<");
// Step 3 — 100%
tl.to(".loading__progress-inner", { scaleY: 1 });
tl.to(".loading__number-group.is--second .loading__number-wrap", {
yPercent: -90,
}, "<");
tl.to(".loading__number-group.is--third .loading__number-wrap", {
yPercent: -90,
}, "<");
tl.to(".loading__number-group.is--first .loading__number-wrap", {
yPercent: 0,
}, "<");
}
document.addEventListener("DOMContentLoaded", () => {
initLoaderThreeSteps();
});Attributes
| Name | Type | Default | Description |
|---|---|---|---|
| .loading__number-group.is--first | class | — | The hundreds digit — contains a single "1". Starts off-screen (yPercent: 100) and slides in only when the counter reaches 100%. |
| .loading__number-group.is--second | class | — | The tens digit strip (1–0). GSAP scrolls this vertically via yPercent to show the correct digit at each step. |
| .loading__number-group.is--third | class | — | The units digit strip (1–0). Works the same as the tens strip — yPercent determines which number is visible. |
| .loading__progress-inner | class | — | The vertical progress bar fill. transform-origin is set to bottom so scaleY: 0 → 1 fills upward. |
| .loading__percentage | class | — | The % symbol. Starts off-screen (yPercent: 100) and slides in alongside the first progress step. |
Notes
- •Requires GSAP loaded via CDN before the script runs. No additional plugins needed.
- •The intermediate values are randomised on each page load using gsap.utils.random(), so the counter always shows a different two-step path to 100%.
- •The digit strips work by setting yPercent on the .loading__number-wrap. Each digit is 1em tall, so yPercent: -10 moves to the next digit. -90 = the "0" at the end of the strip.
- •The .loading-screen starts with display: none and is shown by the first GSAP .set() call — this prevents a flash of the loader before the script runs.
- •Colors (#E2E1DF background, #ff4c24 progress bar) are set directly in CSS — update them to match your design system.