Page Overlay Loader
Original · freefull-page loading overlay with progress
byWeb InnoventixReact + Tailwind
loadpageoverlayloaders
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/load-page-overlay.jsonload-page-overlay.tsx
"use client";
import {
useCallback,
useEffect,
useId,
useRef,
useState,
type KeyboardEvent,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Speed = "slow" | "normal" | "fast";
type Status = "loading" | "complete";
type Note = { kind: "complete" | "cancel"; text: string } | null;
type Stage = { at: number; label: string; detail: string };
const RATES: Record<Speed, number> = { slow: 15, normal: 33, fast: 66 };
const SPEEDS: { id: Speed; label: string }[] = [
{ id: "slow", label: "Slow" },
{ id: "normal", label: "Normal" },
{ id: "fast", label: "Fast" },
];
const TOTAL_MODULES = 128;
const STAGES: Stage[] = [
{ at: 0, label: "Initializing runtime", detail: "Booting the sandbox container" },
{ at: 12, label: "Establishing connection", detail: "Negotiating a secure TLS session" },
{ at: 30, label: "Fetching modules", detail: "Downloading 128 dependencies" },
{ at: 55, label: "Compiling assets", detail: "Bundling scripts and stylesheets" },
{ at: 78, label: "Optimizing interface", detail: "Tree-shaking and minifying output" },
{ at: 92, label: "Finalizing", detail: "Warming the edge cache" },
];
const READY_STAGE: Stage = {
at: 100,
label: "Workspace ready",
detail: "Everything loaded — jump right in.",
};
const FOCUSABLE =
'button:not([disabled]), [href], input:not([disabled]), [tabindex]:not([tabindex="-1"])';
function stageFor(p: number): Stage {
let s = STAGES[0];
for (const st of STAGES) {
if (p >= st.at) s = st;
}
return s;
}
const RING_R = 52;
const RING_C = 2 * Math.PI * RING_R;
export default function LoadPageOverlay() {
const reduce = useReducedMotion();
const uid = useId();
const titleId = `${uid}-title`;
const descId = `${uid}-desc`;
const gradId = `${uid}-grad`;
const [open, setOpen] = useState(false);
const [status, setStatus] = useState<Status>("loading");
const [progress, setProgress] = useState(0);
const [speed, setSpeed] = useState<Speed>("normal");
const [autoClose, setAutoClose] = useState(true);
const [note, setNote] = useState<Note>(null);
const progressRef = useRef(0);
const startRef = useRef(0);
const wasOpen = useRef(false);
const triggerRef = useRef<HTMLButtonElement | null>(null);
const dialogRef = useRef<HTMLDivElement | null>(null);
const cancelRef = useRef<HTMLButtonElement | null>(null);
const enterRef = useRef<HTMLButtonElement | null>(null);
const start = useCallback(() => {
progressRef.current = 0;
startRef.current =
typeof performance !== "undefined" ? performance.now() : Date.now();
setProgress(0);
setNote(null);
setStatus("loading");
setOpen(true);
}, []);
const cancel = useCallback(() => {
setOpen(false);
setStatus("loading");
progressRef.current = 0;
setNote({ kind: "cancel", text: "Load cancelled before completion" });
}, []);
const enter = useCallback(() => {
setOpen(false);
}, []);
// Progress engine: smooth rAF when motion is allowed, discrete steps otherwise.
useEffect(() => {
if (!open || status !== "loading") return;
if (reduce) {
const id = window.setInterval(() => {
const next = Math.min(100, progressRef.current + 12.5);
progressRef.current = next;
setProgress(next);
if (next >= 100) {
window.clearInterval(id);
setStatus("complete");
}
}, 220);
return () => window.clearInterval(id);
}
let raf = 0;
let last = performance.now();
const tick = (now: number) => {
const dt = Math.min(0.05, (now - last) / 1000);
last = now;
const p = progressRef.current;
const ease = 0.35 + 0.65 * ((100 - p) / 100);
const jitter = 0.8 + Math.random() * 0.4;
let next = p + RATES[speed] * dt * ease * jitter;
if (next >= 100) next = 100;
progressRef.current = next;
setProgress(next);
if (next >= 100) {
setStatus("complete");
return;
}
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [open, status, speed, reduce]);
// On completion: record the elapsed time and optionally auto-dismiss.
useEffect(() => {
if (status !== "complete" || !open) return;
const nowMs =
typeof performance !== "undefined" ? performance.now() : Date.now();
const secs = (nowMs - startRef.current) / 1000;
setNote({ kind: "complete", text: `Workspace ready in ${secs.toFixed(1)}s` });
if (autoClose) {
const t = window.setTimeout(() => setOpen(false), 1100);
return () => window.clearTimeout(t);
}
}, [status, open, autoClose]);
// Move focus into the overlay on open; restore it to the trigger on close.
useEffect(() => {
if (open) {
wasOpen.current = true;
const t = window.setTimeout(() => cancelRef.current?.focus(), 40);
return () => window.clearTimeout(t);
}
if (wasOpen.current) {
wasOpen.current = false;
triggerRef.current?.focus();
}
}, [open]);
// When the run completes, park focus on the confirm button.
useEffect(() => {
if (status === "complete" && open) {
const t = window.setTimeout(() => enterRef.current?.focus(), 40);
return () => window.clearTimeout(t);
}
}, [status, open]);
const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Escape") {
e.preventDefault();
cancel();
return;
}
if (e.key !== "Tab" || !dialogRef.current) return;
const nodes = Array.from(
dialogRef.current.querySelectorAll<HTMLElement>(FOCUSABLE),
).filter((n) => n.offsetParent !== null);
if (nodes.length === 0) return;
const first = nodes[0];
const last = nodes[nodes.length - 1];
const active = document.activeElement;
if (e.shiftKey && active === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && active === last) {
e.preventDefault();
first.focus();
}
};
const pct = Math.round(progress);
const stage = status === "complete" ? READY_STAGE : stageFor(progress);
const loaded = Math.min(
TOTAL_MODULES,
Math.round((progress / 100) * TOTAL_MODULES),
);
const liveText =
status === "complete"
? "Loading complete. Your workspace is ready."
: `${stage.label}…`;
const ringOffset = RING_C * (1 - progress / 100);
const ringDuration = reduce ? 0 : 0.28;
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-4 py-16 text-slate-900 sm:py-24 dark:bg-slate-950 dark:text-slate-100">
<style>{`
@keyframes lpo-spin { to { transform: rotate(360deg); } }
@keyframes lpo-shimmer { 0% { transform: translateX(-130%); } 100% { transform: translateX(240%); } }
@keyframes lpo-pulse { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }
@keyframes lpo-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-9px); } }
.lpo-spin { animation: lpo-spin .95s linear infinite; transform-origin: 60px 60px; }
.lpo-shimmer { animation: lpo-shimmer 1.5s ease-in-out infinite; }
.lpo-pulse { animation: lpo-pulse 2.2s ease-in-out infinite; }
.lpo-float { animation: lpo-float 6s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.lpo-spin, .lpo-shimmer, .lpo-pulse, .lpo-float { animation: none !important; }
}
`}</style>
{/* atmosphere */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 opacity-70 [background:radial-gradient(60%_50%_at_15%_0%,theme(colors.indigo.500/0.10),transparent),radial-gradient(50%_50%_at_100%_100%,theme(colors.sky.400/0.10),transparent)]"
/>
<div className="relative mx-auto max-w-5xl">
<header className="mb-8 max-w-2xl">
<p className="mb-3 inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.2em] text-indigo-600 dark:text-indigo-400">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-indigo-500" />
Loaders / Overlay
</p>
<h2 className="text-3xl font-semibold tracking-tight sm:text-4xl">
Full-page load overlay
</h2>
<p className="mt-3 text-base leading-relaxed text-slate-600 dark:text-slate-400">
A focus-trapped loading screen with real, staged progress. Trigger a
simulated deploy, watch it stream through each phase, then cancel or
step into the workspace when it lands.
</p>
</header>
{/* control bar */}
<div className="mb-6 flex flex-col gap-4 rounded-2xl border border-slate-200 bg-white/80 p-4 backdrop-blur sm:flex-row sm:flex-wrap sm:items-center sm:justify-between dark:border-slate-800 dark:bg-slate-900/70">
<div className="flex flex-wrap items-center gap-3">
<button
ref={triggerRef}
type="button"
onClick={start}
disabled={open}
className="inline-flex items-center gap-2 rounded-xl bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-50 dark:focus-visible:ring-offset-slate-900"
>
<BoltIcon className="h-4 w-4" />
{open ? "Loading…" : "Simulate page load"}
</button>
<fieldset
disabled={open}
className="flex items-center gap-1 rounded-xl border border-slate-200 bg-slate-100/70 p-1 disabled:opacity-50 dark:border-slate-700 dark:bg-slate-800/60"
>
<legend className="sr-only">Load speed</legend>
{SPEEDS.map((s) => (
<label
key={s.id}
className="relative cursor-pointer rounded-lg px-3 py-1.5 text-sm font-medium text-slate-600 transition-colors has-[:checked]:bg-white has-[:checked]:text-slate-900 has-[:checked]:shadow-sm dark:text-slate-300 dark:has-[:checked]:bg-slate-950 dark:has-[:checked]:text-white"
>
<input
type="radio"
name={`${uid}-speed`}
value={s.id}
checked={speed === s.id}
onChange={() => setSpeed(s.id)}
className="sr-only [&:focus-visible+span]:outline [&:focus-visible+span]:outline-2 [&:focus-visible+span]:outline-offset-2 [&:focus-visible+span]:outline-indigo-500"
/>
<span className="rounded">{s.label}</span>
</label>
))}
</fieldset>
<label className="inline-flex cursor-pointer items-center gap-2 text-sm font-medium text-slate-700 dark:text-slate-300">
<span className="relative inline-flex">
<input
type="checkbox"
checked={autoClose}
onChange={(e) => setAutoClose(e.target.checked)}
className="peer h-5 w-9 cursor-pointer appearance-none rounded-full bg-slate-300 transition-colors checked:bg-emerald-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-slate-700 dark:focus-visible:ring-offset-slate-900"
/>
<span className="pointer-events-none absolute left-0.5 top-0.5 h-4 w-4 rounded-full bg-white shadow transition-transform peer-checked:translate-x-4" />
</span>
Auto-close
</label>
</div>
<p
aria-live="polite"
className="min-h-[1.25rem] text-sm text-slate-500 dark:text-slate-400"
>
{note?.kind === "complete" && (
<span className="inline-flex items-center gap-1.5 text-emerald-600 dark:text-emerald-400">
<CheckIcon className="h-4 w-4" />
{note.text}
</span>
)}
{note?.kind === "cancel" && (
<span className="inline-flex items-center gap-1.5 text-rose-600 dark:text-rose-400">
<XIcon className="h-4 w-4" />
{note.text}
</span>
)}
</p>
</div>
{/* the "page" being loaded, with the overlay layered on top */}
<div className="relative min-h-[440px] overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-xl shadow-slate-900/5 dark:border-slate-800 dark:bg-slate-900 dark:shadow-black/30">
{/* browser chrome */}
<div className="flex items-center gap-3 border-b border-slate-200 bg-slate-50 px-4 py-3 dark:border-slate-800 dark:bg-slate-950/60">
<div className="flex gap-1.5" aria-hidden="true">
<span className="h-3 w-3 rounded-full bg-rose-400" />
<span className="h-3 w-3 rounded-full bg-amber-400" />
<span className="h-3 w-3 rounded-full bg-emerald-400" />
</div>
<div className="ml-2 flex-1 truncate rounded-md bg-white px-3 py-1 text-xs text-slate-400 ring-1 ring-inset ring-slate-200 dark:bg-slate-900 dark:text-slate-500 dark:ring-slate-700">
app.auroraconsole.io/dashboard
</div>
</div>
{/* skeleton dashboard */}
<div aria-hidden="true" className="space-y-5 p-6 sm:p-8">
<div className="flex items-center justify-between">
<div className="space-y-2">
<div className="h-3 w-40 rounded-full bg-slate-200 dark:bg-slate-800" />
<div className="h-2.5 w-24 rounded-full bg-slate-100 dark:bg-slate-800/60" />
</div>
<div className="h-9 w-9 rounded-full bg-slate-200 dark:bg-slate-800" />
</div>
<div className="grid grid-cols-3 gap-3">
{["Sessions", "Revenue", "Uptime"].map((k) => (
<div
key={k}
className="space-y-3 rounded-xl border border-slate-100 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-800/40"
>
<div className="h-2.5 w-16 rounded-full bg-slate-200 dark:bg-slate-700" />
<div className="h-5 w-20 rounded-md bg-slate-200 dark:bg-slate-700" />
</div>
))}
</div>
<div className="flex h-32 items-end gap-2 rounded-xl border border-slate-100 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-800/40">
{[42, 68, 35, 80, 55, 72, 48, 90, 60, 38, 74, 52].map((h, i) => (
<div
key={i}
style={{ height: `${h}%` }}
className="flex-1 rounded-t bg-slate-200 dark:bg-slate-700"
/>
))}
</div>
</div>
{/* overlay */}
<AnimatePresence>
{open && (
<motion.div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
aria-describedby={descId}
onKeyDown={onKeyDown}
initial={reduce ? { opacity: 1 } : { opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: reduce ? 0 : 0.22 }}
className="absolute inset-0 z-10 flex items-center justify-center bg-white/80 px-6 backdrop-blur-md dark:bg-slate-950/80"
>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 [background:radial-gradient(45%_45%_at_50%_35%,theme(colors.indigo.500/0.14),transparent)]"
/>
<motion.div
initial={reduce ? false : { scale: 0.94, opacity: 0, y: 10 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={reduce ? { opacity: 0 } : { scale: 0.96, opacity: 0, y: 6 }}
transition={{ duration: ringDuration, ease: [0.22, 1, 0.36, 1] }}
className="relative flex w-full max-w-sm flex-col items-center text-center"
>
<p className="mb-6 inline-flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.25em] text-slate-500 dark:text-slate-400">
<span
className={`grid h-6 w-6 place-items-center rounded-md bg-indigo-600 text-white ${reduce ? "" : "lpo-pulse"}`}
>
<BoltIcon className="h-3.5 w-3.5" />
</span>
Aurora Console
</p>
{/* progress ring */}
<div
role="progressbar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={pct}
aria-valuetext={`${pct}% — ${stage.label}`}
aria-labelledby={titleId}
className="relative h-40 w-40"
>
<svg viewBox="0 0 120 120" className="h-full w-full -rotate-90">
<defs>
<linearGradient id={gradId} x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor="#6366f1" />
<stop offset="55%" stopColor="#8b5cf6" />
<stop offset="100%" stopColor="#38bdf8" />
</linearGradient>
</defs>
<circle
cx="60"
cy="60"
r={RING_R}
fill="none"
strokeWidth="9"
className="stroke-slate-200 dark:stroke-slate-800"
/>
<circle
cx="60"
cy="60"
r={RING_R}
fill="none"
strokeWidth="9"
strokeLinecap="round"
stroke={`url(#${gradId})`}
strokeDasharray={RING_C}
strokeDashoffset={ringOffset}
/>
{status === "loading" && !reduce && (
<g className="lpo-spin">
<circle
cx="60"
cy="60"
r={RING_R}
fill="none"
strokeWidth="9"
strokeLinecap="round"
stroke={`url(#${gradId})`}
strokeDasharray={`4 ${RING_C}`}
className="opacity-80"
/>
</g>
)}
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
{status === "complete" ? (
<span className="grid h-12 w-12 place-items-center rounded-full bg-emerald-500/15 text-emerald-600 dark:text-emerald-400">
<CheckIcon className="h-7 w-7" />
</span>
) : (
<>
<span className="text-3xl font-semibold tabular-nums tracking-tight text-slate-900 dark:text-white">
{pct}
<span className="text-lg text-slate-400">%</span>
</span>
<span className="mt-0.5 text-[11px] font-medium uppercase tracking-wider text-slate-400">
loading
</span>
</>
)}
</div>
</div>
<h3
id={titleId}
className="mt-6 text-xl font-semibold tracking-tight text-slate-900 dark:text-white"
>
{status === "complete"
? "Workspace ready"
: "Preparing your workspace"}
</h3>
<p
id={descId}
className="mt-1 min-h-[2.5rem] text-sm text-slate-500 dark:text-slate-400"
>
{stage.detail}
</p>
{/* linear bar */}
<div className="mt-2 w-full">
<div className="relative h-2 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800">
<div
className="relative h-full rounded-full bg-gradient-to-r from-indigo-500 via-violet-500 to-sky-400"
style={{ width: `${progress}%` }}
>
{status === "loading" && !reduce && (
<span className="lpo-shimmer absolute inset-y-0 left-0 w-1/3 bg-gradient-to-r from-transparent via-white/70 to-transparent" />
)}
</div>
</div>
<div className="mt-2 flex items-center justify-between text-xs text-slate-500 dark:text-slate-400">
<span className="tabular-nums">
{loaded} / {TOTAL_MODULES} modules
</span>
<span className="inline-flex items-center gap-1 font-medium capitalize">
<GaugeIcon className="h-3.5 w-3.5" />
{speed}
</span>
</div>
</div>
{/* actions */}
<div className="mt-7 flex w-full items-center justify-center gap-3">
{status === "loading" ? (
<button
ref={cancelRef}
type="button"
onClick={cancel}
className="inline-flex items-center gap-2 rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm font-semibold text-slate-700 transition-colors hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800 dark:focus-visible:ring-offset-slate-950"
>
<XIcon className="h-4 w-4" />
Cancel
</button>
) : (
<button
ref={enterRef}
type="button"
onClick={enter}
className="inline-flex items-center gap-2 rounded-xl bg-emerald-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-emerald-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950"
>
Enter workspace
<ArrowRightIcon className="h-4 w-4" />
</button>
)}
</div>
{status === "loading" && (
<p className="mt-3 text-xs text-slate-400 dark:text-slate-500">
Press{" "}
<kbd className="rounded border border-slate-300 bg-slate-100 px-1.5 py-0.5 font-sans text-[10px] font-semibold text-slate-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300">
Esc
</kbd>{" "}
to cancel
</p>
)}
</motion.div>
<span className="sr-only" role="status" aria-live="polite">
{liveText}
</span>
</motion.div>
)}
</AnimatePresence>
</div>
<p className="mt-5 text-xs text-slate-500 dark:text-slate-500">
Focus is trapped inside the overlay while it loads, <kbd className="rounded border border-slate-300 bg-slate-100 px-1 py-0.5 font-sans text-[10px] font-semibold text-slate-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300">Tab</kbd> cycles its controls, and progress steps discretely when reduced motion is on.
</p>
</div>
</section>
);
}
function BoltIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className={className}>
<path
d="M13 2 4.5 13.5H11l-1 8.5 8.5-11.5H12l1-8.5Z"
fill="currentColor"
/>
</svg>
);
}
function CheckIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={className}
>
<path d="m20 6-11 11-5-5" />
</svg>
);
}
function XIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={className}
>
<path d="M18 6 6 18M6 6l12 12" />
</svg>
);
}
function ArrowRightIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={className}
>
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
);
}
function GaugeIcon({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={className}
>
<path d="M12 14 8 8" />
<path d="M20.4 16a9 9 0 1 0-16.8 0" />
<circle cx="12" cy="14" r="1.2" fill="currentColor" stroke="none" />
</svg>
);
}Dependencies
motion
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Spinner Set Loader
Originalset of circular spinners in sizes

Dots Loader
Originalbouncing dots loader

Bars Loader
Originalequalizer bars loader

Ring Loader
Originalring spinner loader

Pulse Loader
Originalpulsing circle loader

Orbit Loader
Originalorbiting dots loader

Wave Loader
Originalwave loader

Bounce Loader
Originalbouncing ball loader

Gradient Ring Loader
Originalgradient conic ring spinner

Dual Ring Loader
Originaldual counter-rotating rings

Square Loader
Originalmorphing square loader

Typing Loader
Originaltyping indicator loader

