Skip to content
UI ComponentPreloader / Loading ScreenAnimation & EffectNew

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
Preloader Pack — 3 Full-Screen Loading Animations — preview

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

Already linked inside the component's files — no extra setup needed.


Desktop Preview


HTML

html
1<!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

css
1/* 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

javascript
1// 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

Browsers
ChromeEdgeFirefoxSafariOperaBraveArcVivaldi
Devices
DesktopTabletMobile
Frameworks
Vanilla JSHTML/CSS

❓ 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.

MetaSkillGroup

Written by

MetaSkillGroup

October 15, 2026

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.

Preloader Pack: 3 Loading Animations (HTML CSS JS) | MetaSkillGroup