Preloader Pack — 3 Full-Screen Loading Animations
Three full-screen preloaders with their own exit reveals: a big percentage counter with progress line that slides up like a curtain, a morphing gradient blob with bouncing dots that closes into a circle, and bouncing brand letters over vertical stripes that drop away one by one. Each loader is a small build / run / exit object you can drop into any site.
- Published 15 Oct 2026
- 3 views
Overview
Preloader Pack — 3 Full-Screen Loading Animations
First impressions, loaded. Three preloaders with exits as good as their loops — pick one and call it while your page loads.
What's Included
- Counter preloader with uneven, realistic progress and curtain exit
- Morphing blob loader with a circular clip-path reveal
- Wave-letter loader with staggered stripe reveal
- Each loader = build(), run() and exit() — easy to wire to real loading
- Scales to any container with CSS container query units
- Live looping previews plus fullscreen playback
- Skips animation for users who prefer reduced motion
Dependencies
- Google Fonts — Unbounded + DM Sans https://fonts.googleapis.com/css2?family=Unbounded:wght@500;700;800&family=DM+Sans:wght@400;500;700&display=swap
Already linked inside the component's files — no extra setup needed.
Desktop Preview
HTML
html1<!doctype html> 2<html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 <title>Preloader Pack | MetaSkillGroup</title> 7 <link rel="stylesheet" href="style.css" /> 8 </head> 9 <body> 10 <!-- created by metaskillgroup-team --> 11 12 <!-- Fullscreen preloader (plays on page load and from the "Play fullscreen" buttons) --> 13 <div class="overlay" id="overlay" aria-hidden="true"></div> 14 15 <main class="site"> 16 <header class="intro"> 17 <span class="eyebrow">Preloader pack</span> 18 <h1>First impressions, <span>loaded.</span></h1> 19 <p>Three full-screen loading screens with their own exit animations. Each one is a single function — pick one and call it while your page loads.</p> 20 </header> 21 22 <div class="cards"> 23 <article class="card"> 24 <div class="stage" data-loader="counter"></div> 25 <div class="card-meta"> 26 <div> 27 <h2>Counter + curtain</h2> 28 <p>Big percentage, progress line, slides up.</p> 29 </div> 30 <button type="button" class="play" data-play="counter">Play fullscreen</button> 31 </div> 32 </article> 33 34 <article class="card"> 35 <div class="stage" data-loader="morph"></div> 36 <div class="card-meta"> 37 <div> 38 <h2>Morphing shape</h2> 39 <p>Liquid blob, bouncing dots, circle reveal.</p> 40 </div> 41 <button type="button" class="play" data-play="morph">Play fullscreen</button> 42 </div> 43 </article> 44 45 <article class="card"> 46 <div class="stage" data-loader="stripes"></div> 47 <div class="card-meta"> 48 <div> 49 <h2>Wave letters + stripes</h2> 50 <p>Bouncing brand name, striped reveal.</p> 51 </div> 52 <button type="button" class="play" data-play="stripes">Play fullscreen</button> 53 </div> 54 </article> 55 </div> 56 </main> 57 58 <script src="script.js"></script> 59 </body> 60</html>
CSS
css1/* Importing Google Fonts - Unbounded + DM Sans */ 2@import url("https://fonts.googleapis.com/css2?family=Unbounded:wght@500;700;800&family=DM+Sans:wght@400;500;700&display=swap"); 3 4* { 5 margin: 0; 6 padding: 0; 7 box-sizing: border-box; 8 font-family: "DM Sans", sans-serif; 9} 10 11:root { 12 --bg: #0e0f12; 13 --panel: #16181d; 14 --border: #262a33; 15 --text: #f1f2f4; 16 --muted: #8d93a1; 17 --lime: #c6ff3d; 18 --pink: #ff5c8a; 19 --blue: #5b8cff; 20} 21 22body { 23 min-height: 100vh; 24 background: var(--bg); 25 color: var(--text); 26} 27 28body.is-loading { 29 overflow: hidden; 30} 31 32.site { 33 max-width: 1180px; 34 margin: 0 auto; 35 padding: 80px 20px; 36} 37 38/* ---------- Intro ---------- */ 39.intro { 40 max-width: 680px; 41 margin: 0 auto 54px; 42 text-align: center; 43} 44 45.eyebrow { 46 color: var(--lime); 47 font-size: 0.8rem; 48 font-weight: 700; 49 letter-spacing: 0.16em; 50 text-transform: uppercase; 51} 52 53.intro h1 { 54 margin: 16px 0 14px; 55 font-family: "Unbounded", sans-serif; 56 font-size: clamp(1.9rem, 4.6vw, 3.1rem); 57 font-weight: 700; 58 line-height: 1.1; 59 letter-spacing: -0.02em; 60 text-wrap: balance; 61} 62 63.intro h1 span { 64 color: var(--lime); 65} 66 67.intro p { 68 color: var(--muted); 69 font-size: 1.05rem; 70 line-height: 1.65; 71} 72 73/* ---------- Cards ---------- */ 74.cards { 75 display: grid; 76 grid-template-columns: repeat(3, 1fr); 77 gap: 22px; 78} 79 80.card { 81 border: 1px solid var(--border); 82 border-radius: 22px; 83 background: var(--panel); 84 overflow: hidden; 85} 86 87.stage { 88 position: relative; 89 aspect-ratio: 16 / 11; 90 overflow: hidden; 91 background: #0a0b0d; 92 container-type: size; 93} 94 95.card-meta { 96 display: flex; 97 justify-content: space-between; 98 align-items: center; 99 gap: 14px; 100 padding: 18px 20px 20px; 101} 102 103.card-meta h2 { 104 font-size: 1.02rem; 105} 106 107.card-meta p { 108 margin-top: 3px; 109 color: var(--muted); 110 font-size: 0.86rem; 111} 112 113.play { 114 flex-shrink: 0; 115 padding: 10px 14px; 116 border: 0; 117 border-radius: 12px; 118 background: var(--lime); 119 color: #11140a; 120 font-size: 0.85rem; 121 font-weight: 700; 122 cursor: pointer; 123 transition: transform 0.2s; 124} 125 126.play:hover { 127 transform: translateY(-2px); 128} 129 130.play:focus-visible { 131 outline: 3px solid rgba(198, 255, 61, 0.5); 132 outline-offset: 3px; 133} 134 135/* The little fake page revealed behind each preview loader */ 136.mini-site { 137 position: absolute; 138 inset: 0; 139 padding: 6cqmin 7cqmin; 140 background: linear-gradient(160deg, #f6f7fb, #e8ebf5); 141} 142 143.mini-site i, 144.mini-site b { 145 display: block; 146 border-radius: 1.4cqmin; 147 background: #d7dbe8; 148} 149 150.ms-nav { 151 display: flex; 152 gap: 3cqmin; 153 justify-content: flex-end; 154} 155 156.ms-nav i { 157 width: 9cqmin; 158 height: 2.2cqmin; 159} 160 161.ms-nav i:first-child { 162 width: 14cqmin; 163 margin-right: auto; 164 background: #11140a; 165} 166 167.ms-hero { 168 display: grid; 169 gap: 2.4cqmin; 170 margin: 9cqmin 0 7cqmin; 171} 172 173.ms-hero b { 174 width: 70%; 175 height: 6cqmin; 176 background: #11140a; 177} 178 179.ms-hero b + b { 180 width: 50%; 181} 182 183.ms-hero i { 184 width: 22cqmin; 185 height: 6cqmin; 186 margin-top: 1cqmin; 187 border-radius: 2cqmin; 188 background: #a3e635; 189} 190 191.ms-cards { 192 display: grid; 193 grid-template-columns: repeat(3, 1fr); 194 gap: 3cqmin; 195} 196 197.ms-cards i { 198 height: 18cqmin; 199 background: #fff; 200 box-shadow: 0 1cqmin 3cqmin rgba(17, 20, 10, 0.08); 201} 202 203/* ---------- Fullscreen overlay ---------- */ 204.overlay { 205 position: fixed; 206 inset: 0; 207 z-index: 100; 208 container-type: size; 209} 210 211.overlay:empty { 212 display: none; 213} 214 215/* Every loader fills its container (overlay or stage) and scales with it */ 216.loader { 217 position: absolute; 218 inset: 0; 219 display: grid; 220 place-items: center; 221 overflow: hidden; 222} 223 224/* ===== Loader 1 · Counter + curtain ===== */ 225.ld-counter { 226 background: var(--lime); 227 color: #11140a; 228} 229 230.ld-counter .box { 231 width: 70cqw; 232} 233 234.ld-counter .label { 235 display: flex; 236 justify-content: space-between; 237 font-size: max(11px, 2.2cqmin); 238 font-weight: 700; 239 letter-spacing: 0.12em; 240 text-transform: uppercase; 241} 242 243.ld-counter .num { 244 display: block; 245 font-family: "Unbounded", sans-serif; 246 font-size: 30cqmin; 247 font-weight: 800; 248 line-height: 0.9; 249 letter-spacing: -0.05em; 250 font-variant-numeric: tabular-nums; 251} 252 253.ld-counter .num small { 254 font-family: "Unbounded", sans-serif; 255 font-size: 0.35em; 256 letter-spacing: 0; 257} 258 259.ld-counter .bar { 260 height: max(3px, 0.8cqmin); 261 margin-top: 3cqmin; 262 background: rgba(17, 20, 10, 0.15); 263} 264 265.ld-counter .bar i { 266 display: block; 267 width: 100%; 268 height: 100%; 269 background: #11140a; 270 transform: scaleX(var(--p, 0)); 271 transform-origin: left; 272} 273 274/* ===== Loader 2 · Morphing shape ===== */ 275.ld-morph { 276 background: radial-gradient(circle at 50% 40%, #1b1f3a, #090a12 70%); 277} 278 279.ld-morph .inner { 280 display: grid; 281 justify-items: center; 282 gap: 5cqmin; 283} 284 285.ld-morph .shape { 286 width: 26cqmin; 287 aspect-ratio: 1; 288 background: linear-gradient(135deg, var(--pink), var(--blue)); 289 box-shadow: 0 0 10cqmin rgba(255, 92, 138, 0.45); 290 animation: morph 2.4s ease-in-out infinite; 291} 292 293@keyframes morph { 294 0%, 295 100% { 296 border-radius: 50%; 297 transform: rotate(0deg) scale(1); 298 } 299 25% { 300 border-radius: 30% 70% 70% 30% / 30% 30% 70% 70%; 301 transform: rotate(90deg) scale(0.9); 302 } 303 50% { 304 border-radius: 18%; 305 transform: rotate(180deg) scale(1.05); 306 } 307 75% { 308 border-radius: 70% 30% 50% 50% / 40% 60% 40% 60%; 309 transform: rotate(270deg) scale(0.95); 310 } 311} 312 313.ld-morph .dots { 314 display: flex; 315 gap: 2cqmin; 316} 317 318.ld-morph .dots i { 319 width: max(6px, 2cqmin); 320 aspect-ratio: 1; 321 border-radius: 50%; 322 background: #fff; 323 animation: bounce 0.9s ease-in-out infinite; 324} 325 326.ld-morph .dots i:nth-child(2) { 327 animation-delay: 0.15s; 328} 329 330.ld-morph .dots i:nth-child(3) { 331 animation-delay: 0.3s; 332} 333 334@keyframes bounce { 335 0%, 336 100% { 337 transform: translateY(0); 338 opacity: 0.4; 339 } 340 50% { 341 transform: translateY(-2.5cqmin); 342 opacity: 1; 343 } 344} 345 346.ld-morph p { 347 color: #b8bdd6; 348 font-size: max(11px, 2.6cqmin); 349 letter-spacing: 0.08em; 350} 351 352/* ===== Loader 3 · Wave letters + stripes ===== */ 353.ld-stripes { 354 background: transparent; 355} 356 357.ld-stripes .stripes { 358 position: absolute; 359 inset: 0; 360 display: flex; 361} 362 363.ld-stripes .stripes i { 364 flex: 1; 365 height: 100%; 366 margin-right: -1px; 367 background: #121318; 368} 369 370.ld-stripes .stripes i:nth-child(even) { 371 background: #17181f; 372} 373 374.ld-stripes .word { 375 position: relative; 376 display: flex; 377 gap: 0.4cqw; 378 font-family: "Unbounded", sans-serif; 379 font-size: 9cqmin; 380 font-weight: 800; 381} 382 383.ld-stripes .word span { 384 display: inline-block; 385 font-family: "Unbounded", sans-serif; 386 color: var(--text); 387 animation: wave 1.2s ease-in-out infinite; 388 animation-delay: calc(var(--i) * 0.08s); 389} 390 391.ld-stripes .word span:nth-child(3n + 1) { 392 color: var(--lime); 393} 394 395@keyframes wave { 396 0%, 397 60%, 398 100% { 399 transform: translateY(0); 400 } 401 30% { 402 transform: translateY(-4cqmin); 403 } 404} 405 406.ld-stripes .hint { 407 position: absolute; 408 bottom: 8cqmin; 409 color: var(--muted); 410 font-size: max(11px, 2.2cqmin); 411 letter-spacing: 0.2em; 412 text-transform: uppercase; 413} 414 415/* ---------- Responsive ---------- */ 416@media (max-width: 980px) { 417 .cards { 418 grid-template-columns: 1fr; 419 max-width: 520px; 420 margin: 0 auto; 421 } 422} 423 424@media (prefers-reduced-motion: reduce) { 425 *, 426 *::before, 427 *::after { 428 animation-duration: 0.01ms !important; 429 animation-iteration-count: 1 !important; 430 } 431}
JavaScript
javascript1// Each preloader = build (markup) + run (the "loading" part) + exit (the reveal). 2// Use one on your own site like this: 3// const root = loaders.counter.build(overlay); 4// await loaders.counter.run(root); // or wait for your real assets 5// await loaders.counter.exit(root); 6 7const BRAND = "METASKILL"; // letters for the wave loader 8const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; 9const wait = (ms) => new Promise((r) => setTimeout(r, ms)); 10 11const loaders = { 12 counter: { 13 build(container) { 14 container.insertAdjacentHTML( 15 "beforeend", 16 `<div class="loader ld-counter"> 17 <div class="box"> 18 <div class="label"><span>Loading experience</span><span>2026</span></div> 19 <span class="num">0<small>%</small></span> 20 <div class="bar"><i></i></div> 21 </div> 22 </div>`, 23 ); 24 return container.lastElementChild; 25 }, 26 // Counts to 100 in uneven steps so it feels like real loading 27 run(root, duration = 2400) { 28 const num = root.querySelector(".num"); 29 const bar = root.querySelector(".bar"); 30 return new Promise((resolve) => { 31 let value = 0; 32 const tick = () => { 33 value = Math.min(100, value + Math.ceil(Math.random() * 9)); 34 num.innerHTML = `${value}<small>%</small>`; 35 bar.style.setProperty("--p", value / 100); 36 if (value < 100) setTimeout(tick, duration / 22 + Math.random() * 60); 37 else setTimeout(resolve, 250); 38 }; 39 tick(); 40 }); 41 }, 42 exit(root) { 43 return root.animate([{ transform: "translateY(0)" }, { transform: "translateY(-100%)" }], { 44 duration: 800, 45 easing: "cubic-bezier(0.76, 0, 0.24, 1)", 46 fill: "forwards", 47 }).finished; 48 }, 49 }, 50 51 morph: { 52 build(container) { 53 container.insertAdjacentHTML( 54 "beforeend", 55 `<div class="loader ld-morph"> 56 <div class="inner"> 57 <div class="shape"></div> 58 <div class="dots"><i></i><i></i><i></i></div> 59 <p>Preparing your space</p> 60 </div> 61 </div>`, 62 ); 63 return container.lastElementChild; 64 }, 65 run: () => wait(2600), 66 // The whole screen shrinks into a circle and disappears 67 exit(root) { 68 return root.animate( 69 [{ clipPath: "circle(75% at 50% 50%)" }, { clipPath: "circle(0% at 50% 50%)" }], 70 { duration: 800, easing: "cubic-bezier(0.7, 0, 0.3, 1)", fill: "forwards" }, 71 ).finished; 72 }, 73 }, 74 75 stripes: { 76 build(container) { 77 const letters = [...BRAND].map((ch, i) => `<span style="--i:${i}">${ch}</span>`).join(""); 78 container.insertAdjacentHTML( 79 "beforeend", 80 `<div class="loader ld-stripes"> 81 <div class="stripes">${"<i></i>".repeat(7)}</div> 82 <div class="word">${letters}</div> 83 <p class="hint">Loading</p> 84 </div>`, 85 ); 86 return container.lastElementChild; 87 }, 88 run: () => wait(2400), 89 // Text fades, then each stripe drops one after another 90 async exit(root) { 91 const text = [root.querySelector(".word"), root.querySelector(".hint")]; 92 await Promise.all(text.map((el) => el.animate([{ opacity: 1 }, { opacity: 0, transform: "translateY(-12px)" }], { duration: 300, fill: "forwards" }).finished)); 93 const stripes = [...root.querySelectorAll(".stripes i")]; 94 await Promise.all( 95 stripes.map( 96 (s, i) => 97 s.animate([{ transform: "translateY(0)" }, { transform: "translateY(100%)" }], { 98 duration: 650, 99 delay: i * 70, 100 easing: "cubic-bezier(0.7, 0, 0.3, 1)", 101 fill: "forwards", 102 }).finished, 103 ), 104 ); 105 }, 106 }, 107}; 108 109// ---------- Fullscreen playback ---------- 110const overlay = document.getElementById("overlay"); 111let busy = false; 112 113const playFullscreen = async (name) => { 114 if (busy) return; 115 busy = true; 116 document.body.classList.add("is-loading"); 117 const loader = loaders[name]; 118 const root = loader.build(overlay); 119 await loader.run(root); 120 if (!reduceMotion) await loader.exit(root); 121 root.remove(); 122 document.body.classList.remove("is-loading"); 123 busy = false; 124}; 125 126document.querySelectorAll("[data-play]").forEach((btn) => { 127 btn.addEventListener("click", () => playFullscreen(btn.dataset.play)); 128}); 129 130// ---------- Looping previews inside the cards ---------- 131// A tiny fake page sits behind each loader so the reveal has something to show 132const MINI_SITE = ` 133 <div class="mini-site" aria-hidden="true"> 134 <div class="ms-nav"><i></i><i></i><i></i><i></i></div> 135 <div class="ms-hero"><b></b><b></b><i></i></div> 136 <div class="ms-cards"><i></i><i></i><i></i></div> 137 </div>`; 138 139const loopPreview = async (stage) => { 140 const loader = loaders[stage.dataset.loader]; 141 stage.innerHTML = MINI_SITE; 142 for (;;) { 143 const root = loader.build(stage); 144 await loader.run(root); 145 if (reduceMotion) return; 146 await loader.exit(root); 147 await wait(1400); 148 root.remove(); 149 } 150}; 151 152document.querySelectorAll(".stage").forEach((stage, i) => setTimeout(() => loopPreview(stage), i * 500)); 153 154// Play the counter preloader once when the page opens 155if (!reduceMotion) playFullscreen("counter");
Built by MetaSkillGroup — copy it, use it, make it yours.
Features
Counter preloader with uneven, realistic progress and curtain exit
Morphing blob loader with a circular clip-path reveal
Wave-letter loader with staggered stripe reveal
Each loader = build(), run() and exit() — easy to wire to real loading
Scales to any container with CSS container query units
Live looping previews plus fullscreen playback
Skips animation for users who prefer reduced motion
Built With
HTML5
CSS3
JavaScript
Compatibility
❓ FAQs
Build the loader into the overlay, then instead of loader.run() wait for window's load event (or your data fetch), and finally call loader.exit().
Yes — change the BRAND constant at the top of script.js. Every letter gets its own bounce delay automatically.
Keep the overlay div, the CSS for that loader (.ld-counter, .ld-morph or .ld-stripes) and its entry in the loaders object. Call playFullscreen('counter') or whichever you kept.

Written by
MetaSkillGroup
Need it built for you?
Built to the same standard as our client work.
If nothing here fits exactly, tell us what you need — our core developers build it. Requirements call first, a written SRS before any code, and a price agreed up front.