Loading Overlay — Free HTML CSS JS Snippet
Loading Overlay · Loaders · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="page">
<div class="content-card">
<h2>Dashboard Overview</h2>
<p>This card represents your page content.</p>
<div class="mini-grid">
<div class="stat-block"><div class="stat-n">$48,295</div><div class="stat-l">Revenue</div></div>
<div class="stat-block"><div class="stat-n">1,284</div><div class="stat-l">Users</div></div>
<div class="stat-block"><div class="stat-n">94.2%</div><div class="stat-l">Uptime</div></div>
</div>
</div>
<div class="trigger-row">
<button class="trigger-btn" onclick="showOverlay('spinner')">Spinner overlay</button>
<button class="trigger-btn" onclick="showOverlay('dots')">Dots overlay</button>
<button class="trigger-btn" onclick="showOverlay('progress')">Progress overlay</button>
<button class="trigger-btn outline" onclick="hideOverlay()">Hide</button>
</div>
<!-- Overlay -->
<div class="overlay" id="overlay">
<div class="overlay-box" id="overlay-box">
<!-- Spinner -->
<div class="loader-wrap" id="l-spinner">
<div class="spinner"></div>
<div class="overlay-msg">Loading data…</div>
</div>
<!-- Dots -->
<div class="loader-wrap hidden" id="l-dots">
<div class="dots"><span></span><span></span><span></span></div>
<div class="overlay-msg">Processing…</div>
</div>
<!-- Progress -->
<div class="loader-wrap hidden" id="l-progress">
<div class="overlay-msg">Uploading files…</div>
<div class="prog-track"><div class="prog-fill" id="prog-fill"></div></div>
<div class="prog-pct" id="prog-pct">0%</div>
</div>
</div>
</div>
</div>* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #f1f5f9; min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 24px; }
.page { width: 100%; max-width: 500px; position: relative; display: flex; flex-direction: column; gap: 16px; }
.content-card { background: #fff; border-radius: 16px; padding: 24px; border: 1px solid #e2e8f0; }
.content-card h2 { font-size: 18px; font-weight: 800; color: #0f172a; margin-bottom: 6px; }
.content-card p { font-size: 13px; color: #64748b; margin-bottom: 20px; }
.mini-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 12px; }
.stat-block { text-align: center; }
.stat-n { font-size: 18px; font-weight: 800; color: #0f172a; }
.stat-l { font-size: 11px; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.5px; }
.trigger-row { display: flex; gap: 8px; flex-wrap: wrap; }
.trigger-btn { background: #6366f1; color: #fff; border: none; border-radius: 9px; padding: 9px 16px; font-size: 13px; font-weight: 600; cursor: pointer; transition: background 0.12s; }
.trigger-btn:hover { background: #4f46e5; }
.trigger-btn.outline { background: transparent; color: #475569; border: 1.5px solid #e2e8f0; }
.trigger-btn.outline:hover { border-color: #6366f1; color: #6366f1; }
/* Overlay */
.overlay { position: absolute; inset: 0; background: rgba(255,255,255,0.85); backdrop-filter: blur(6px); border-radius: 16px; display: flex; align-items: center; justify-content: center; opacity: 0; pointer-events: none; transition: opacity 0.25s; z-index: 10; }
.overlay.visible { opacity: 1; pointer-events: all; }
.overlay-box { display: flex; flex-direction: column; align-items: center; gap: 14px; }
.loader-wrap { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.loader-wrap.hidden { display: none; }
.overlay-msg { font-size: 14px; font-weight: 600; color: #475569; }
/* Spinner */
.spinner { width: 40px; height: 40px; border: 3px solid #e2e8f0; border-top-color: #6366f1; border-radius: 50%; animation: spin 0.8s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* Dots */
.dots { display: flex; gap: 7px; }
.dots span { width: 10px; height: 10px; border-radius: 50%; background: #6366f1; animation: bounce 1.2s ease-in-out infinite; }
.dots span:nth-child(2) { animation-delay: 0.2s; }
.dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce { 0%,100%{transform:translateY(0);opacity:0.4} 50%{transform:translateY(-10px);opacity:1} }
/* Progress */
.prog-track { width: 220px; height: 6px; background: #e2e8f0; border-radius: 3px; overflow: hidden; }
.prog-fill { height: 100%; background: linear-gradient(90deg,#6366f1,#10b981); border-radius: 3px; width: 0%; transition: width 0.3s ease; }
.prog-pct { font-size: 12px; font-weight: 700; color: #6366f1; }let progInterval = null;
function showOverlay(type) {
clearInterval(progInterval);
['l-spinner','l-dots','l-progress'].forEach(id => {
document.getElementById(id).classList.add('hidden');
});
document.getElementById('l-' + type).classList.remove('hidden');
document.getElementById('overlay').classList.add('visible');
if (type === 'progress') {
let pct = 0;
document.getElementById('prog-fill').style.width = '0%';
document.getElementById('prog-pct').textContent = '0%';
progInterval = setInterval(() => {
pct += Math.random() * 8 + 2;
if (pct >= 100) { pct = 100; clearInterval(progInterval); setTimeout(hideOverlay, 600); }
document.getElementById('prog-fill').style.width = pct.toFixed(0) + '%';
document.getElementById('prog-pct').textContent = pct.toFixed(0) + '%';
}, 300);
} else {
setTimeout(hideOverlay, 3000);
}
}
function hideOverlay() {
clearInterval(progInterval);
document.getElementById('overlay').classList.remove('visible');
}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.
What's included
Features
About this UI Snippet
Loading Overlay — Spinner, Dots & Progress Bar Variants with Blur Backdrop and Opacity Fade

A loading overlay blocks a section or full page during an asynchronous operation — API call, file upload, data processing — to prevent user interaction with partially loaded content and communicate that something is happening. This snippet provides three overlay variants: a classic CSS spinner, bouncing dots, and a simulated progress bar — all with a frosted glass backdrop-filter blur and opacity fade transition.
The overlay backdrop
The overlay uses position: absolute; inset: 0 to cover the parent container entirely. background: rgba(255,255,255,0.85) provides a semi-transparent white tint. backdrop-filter: blur(6px) blurs the content behind the overlay — the frosted glass effect. In the closed state, opacity: 0 and pointer-events: none make the overlay invisible and non-interactive. Adding .visible switches to opacity: 1 and pointer-events: all. The CSS transition: opacity 0.25s fades smoothly.
Spinner variant
The spinner uses border-top-color: #6366f1 on an otherwise light grey bordered circle, rotated continuously via a CSS keyframe animation. The cubic-bezier timing is linear for a consistent revolution speed.
Dots variant
Three bouncing dots use translateY(-10px) at the 50% keyframe with staggered animation-delay (0s, 0.2s, 0.4s). Opacity fades from 0.4 to 1 in sync.
Progress bar variant
A simulated setInterval advances the progress bar by a random amount (2–10%) every 300ms. When it reaches 100%, the interval clears and hideOverlay() fires after a 600ms delay. In production, replace the simulation with a real upload progress event: xhr.upload.onprogress = e => { const pct = e.loaded/e.total*100; updateProgress(pct); }.
Positioning: absolute vs fixed
The overlay uses position: absolute to cover only the parent .page container. For a full-page overlay, change to position: fixed; inset: 0; z-index: 9999 and attach it to the document body instead of a container div. The blur and opacity behaviour is identical — only the covered area changes.
Performance note
backdrop-filter: blur() is GPU-composited but can be expensive on older mobile devices. If performance is a concern, replace backdrop-filter with background: rgba(255,255,255,0.94) without blur. Test with Chrome DevTools Performance to confirm the overlay renders without triggering main-thread layout recalculation. All three loader animations (spinner rotation, dots bounce, progress bar fill) use transform and width — both GPU-safe properties that avoid layout recalculation on each animation frame. For mobile-first projects, always prefer transform-based animations over properties that trigger layout or paint — this principle applies equally to the overlay's loader variants and any other CSS animation throughout the codebase.
Step by step
How to Use
- 1Click any trigger button to see the overlay variantSpinner overlay shows for 3 seconds then auto-hides. Dots overlay shows for 3 seconds then auto-hides. Progress overlay simulates a file upload, advancing the bar and auto-closing at 100%.
- 2Wrap your content in a position:relative containerThe overlay covers the nearest position:relative ancestor. Ensure your .page or section container has position: relative set. The overlay uses position: absolute; inset: 0 to fill it exactly.
- 3Wire to a real API call or upload eventShow on fetch start: showOverlay("spinner"). Hide on .then() or .finally(): hideOverlay(). For uploads: show progress variant, then wire xhr.upload.onprogress to updateProgress(pct). Hide on upload complete.
- 4Convert to a full-page overlayChange position: absolute to position: fixed on .overlay. Remove border-radius: 16px. The overlay now covers the entire viewport instead of just the parent container.
- 5Change the overlay opacity and blurUpdate rgba(255,255,255,0.85) to control the backdrop tint opacity. Increase or decrease blur(6px) on backdrop-filter. For dark-themed pages, use rgba(0,0,0,0.6) for a dark overlay with white spinner.
- 6Export in your formatClick "HTML" for a standalone file, "JSX" for a React component with visible/type state managed by useState, or "Tailwind" for a Tailwind CSS version.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Replace the setInterval simulation with an XMLHttpRequest upload progress listener: const xhr = new XMLHttpRequest(); xhr.upload.onprogress = e => { if (e.lengthComputable) { const pct = Math.round(e.loaded/e.total*100); document.getElementById("prog-fill").style.width = pct + "%"; document.getElementById("prog-pct").textContent = pct + "%"; if (pct >= 100) setTimeout(hideOverlay, 600); } }; xhr.open("POST", "/upload"); xhr.send(formData). Show the progress overlay before calling xhr.send().
Move the .overlay div to be a direct child of body (not inside the .page container). Change position: absolute to position: fixed in the CSS and remove border-radius: 16px. Now the overlay covers the full viewport. Set z-index: 9999 to ensure it appears above all page content. The opacity fade and blur work identically — only the covered area changes.
Change background: rgba(255,255,255,0.85) to background: rgba(0,0,0,0.6) on .overlay. Change .overlay-msg color to #f1f5f9. Change .spinner border colour to rgba(255,255,255,0.15) and border-top-color to #fff. The dots and progress bar can remain the same since they use the accent colour which reads on dark.
Manage loading state with useState: const [loading, setLoading] = useState({ visible: false, type: "spinner" }). Render the overlay: {loading.visible && <div className="overlay visible">...</div>}. On API call: setLoading({visible:true, type:"spinner"}) before fetch, setLoading({visible:false, type:"spinner"}) in finally(). Pass the loading type to conditionally render the correct variant inside the overlay box.