webdevpuneet.com

Full-Screen Percentage Counter Loader — Eased 0-100% Takeover

Full-Screen Percentage Counter Loader · Loaders · Plain HTML, CSS & JS · Live preview

CategoryLoaders

Copy & paste

Source Code

<div class="pm-overlay" id="pmOverlay">
  <div class="pm-num" id="pmNum">0<span class="pm-sign">%</span></div>
  <div class="pm-label" id="pmLabel">Loading experience…</div>
</div>
<div class="pm-page">
  <h1>Welcome back</h1>
  <p>The real page content sits underneath the takeover.</p>
  <button type="button" class="pm-replay" id="pmReplay">↻ Replay loader</button>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

rAF-driven counting
A requestAnimationFrame loop, not setInterval, drives the whole animation.
Real cubic ease-out
easeOutCubic shapes the count to race early and decelerate into 100%.
Change-tied digit pulse
The scale pulse fires only when the displayed integer actually changes.
Force-reflow restart
void offsetWidth guarantees the pulse animation replays on every change.
Threshold-based status text
Labels derive from the same eased percentage, never drifting from the number.
Full-viewport takeover
A fixed-position overlay sits above the real page content underneath.
Hold-then-fade completion
A brief pause on Ready before the overlay fades and unblocks pointer events.
Tunable and data-drivable
Swap the fixed duration for real asset-load progress with no structural change.

About this UI Snippet

Full-Screen Percentage Counter Loader — Eased Digits, Pulse-on-Update, Fade-to-Reveal

Screenshot of the Full-Screen Percentage Counter Loader snippet rendered live

A handful of premium sites — portfolios, product launches, agency homepages — greet visitors with a full-screen percentage counting up before the real page fades in. Done badly it's a linear digit-flip that feels mechanical; done well the count decelerates like it's genuinely arriving somewhere, and each digit change has a tiny physical pulse. This snippet builds the effect properly: a requestAnimationFrame loop driven by a real cubic ease-out, a scale pulse on every number change, staged status text, and a fade-out that reveals the underlying page — all in plain HTML, CSS, and vanilla JavaScript.

Why requestAnimationFrame with real easing, not setInterval

The counter is driven entirely by runCounter()'s frame() function inside a requestAnimationFrame loop, not a setInterval incrementing by one. Each frame computes elapsed time as a fraction of a fixed 2.6s duration, runs it through easeOutCubic(t) = 1 - (1 - t)³, and multiplies by 100 to get the displayed percentage. Because the easing curve's derivative is steep near t = 0 and flattens toward t = 1, the count visibly races through the early numbers and decelerates into a gentle settle at 100 — the same "arriving, not ticking" feel a countdown clock lacks. A linear count (no easing function at all) would tick at a constant rate and read as mechanical rather than deliberate.

A pulse tied to actual digit changes, not a fixed timer

Every time the displayed integer percentage changes — checked with pct !== lastShown so a frame that rounds to the same number never re-triggers anything — the number element's pm-pulse class is removed, the layout is force-reflowed with void numEl.offsetWidth, and the class is re-added, restarting a quick scale(1.06) CSS transition. This ties the visual "beat" of the animation to genuine data changes rather than a separate decorative timer running alongside the count, so the pulse and the number are always perfectly synchronized, even though the frame rate driving them isn't fixed.

Staged status text underneath the number

A small LABELS lookup swaps a status line ("Loading experience…", "Fetching assets…", "Almost ready…", "Ready") at percentage thresholds, computed by labelFor() from the same eased percentage driving the digits — so the label text and the number are always in lockstep, derived from one shared value rather than a second independent timer that could drift out of sync.

Full-viewport takeover and reveal

The counter sits in a position: fixed; inset: 0 overlay above the real page content, which renders normally underneath. Once the animation reaches t = 1, a short pause holds the finished "100% / Ready" state, then pm-hide fades the whole overlay's opacity to 0 with pointer-events: none, revealing the page beneath — the counter itself never has to know what page it's covering.

Tuning and reuse

Change duration for a snappier or slower count, swap easeOutCubic for a different easing function (a bounce or an elastic ease reads very differently), or drive the percentage from real asset-loading progress (a window.onload, a resource count, or a preloader library's callback) instead of a fixed-duration animation — the eased-display and pulse logic works identically either way. Pair it with a loading overlay for shorter in-page waits or a top loading bar for route transitions.

Step by step

How to Use

  1. 1
    Paste HTML, CSS, and JSA full-screen dark overlay appears with a huge "0%" and begins counting immediately.
  2. 2
    Watch it accelerate then settleThe count races through the early numbers and eases into 100% rather than ticking at a constant rate.
  3. 3
    Notice the pulseEvery digit change gives the number a quick scale pulse, tied to the actual value changing.
  4. 4
    Watch the status textThe label beneath the number advances through stages as the percentage crosses thresholds.
  5. 5
    See it fade and revealAt 100%, it holds briefly on "Ready" then fades out to reveal the real page.
  6. 6
    Replay itClick "Replay loader" to re-run the entire counting sequence from zero.

Real-world uses

Common Use Cases

Agency and portfolio homepages
Greet visitors with one huge percentage counting to 100, then fade to reveal the page for a premium first impression.
Product launch teasers
Build anticipation before a reveal, with easeOutCubic racing early and decelerating into 100 so the count feels earned, not mechanical.
Heavy 3D scene preloaders
Tie the number to real asset-loading progress for a WebGL experience, and pair with a loading overlay for the fade out.
Branded app splash takeovers
Cover first-load data fetching with a bold number that pulses only when the displayed integer actually changes.
Route transition alternatives
Offer a full-screen option next to a top loading bar, with void offsetWidth forcing a reflow so every pulse replays.
Related: Multi-Stage Loading Checklist
See the Multi-Stage Loading Checklist for a related loaders pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

requestAnimationFrame runs in sync with the browser's paint cycle, giving smooth, frame-accurate timing, and it naturally supports computing progress as elapsed-time-over-duration each frame — which is what makes real easing possible. A setInterval incrementing by a fixed step every tick can only count linearly and drifts from real elapsed time if the tab is throttled.

easeOutCubic(t) = 1 - (1 - t)³ takes a linear progress fraction from 0 to 1 and reshapes it so the output changes quickly near t = 0 and slows sharply near t = 1. Multiplying that eased value by 100 produces a percentage that visibly races through the early numbers and settles gently into 100 — a real mathematical curve, not a scripted slowdown.

The pulse is gated on pct !== lastShown, so it only fires when the displayed integer percentage genuinely changes — since requestAnimationFrame can run many times between one integer and the next, most frames update nothing visually. This keeps the pulse tied to real data changes rather than firing dozens of times a second regardless of whether the number moved.

Replace the elapsed-time calculation in frame() with your actual progress source — for example a running count of resources loaded divided by total resources, or values from a preloading library's progress callback — and skip the eased time-based ramp entirely, applying easeOutCubic (or no easing) directly to that real fraction instead. The pulse, label, and fade-out logic all stay identical.

Hold the displayed percentage in state and run the same requestAnimationFrame loop inside a mount effect, updating state only when the rounded integer changes so re-renders match the pulse-trigger condition exactly. Clean up by cancelling the animation frame on unmount. The CSS transitions and full-viewport overlay markup port unchanged.