All components

Kudos Landing Page Reveal

GSAP animation component · Published 2026-07-27 · by vanguardia.dev

Open live demo ↗ Raw prompt (.md)

What it does

A fullscreen preloader plays automatically on load: a randomized 0-to-100% counter runs while a stack of ten portrait images wipes across the frame via animated 'left' offsets, then GSAP power3.inOut tweens slide the counter out and a masked logo in, fade the overlay away, scale the hero image from 2 to 1, and slide the giant hero name and top nav into place. Triggered once on page load, roughly 8 seconds total.

How it's built

Categorypreloader
Techgsap
Complexitypage
Performance costlight
Mobile-safeyes

preloader reveal counter clip-path gsap landing hero stagger intro

Rebuild it with AI

To reproduce this animation in your own project, copy the prompt below into Claude Code, Cursor or any AI coding agent. The prompt is validated — it describes the exact structure, timing and easing, so the agent rebuilds the effect faithfully and you can then adapt colors, copy and layout to your design.

The full prompt

Landing Page Reveal — Counter Preloader + Sliding Image Stack + Masked Hero Unveil

Goal

Build a full-viewport editorial landing hero fronted by a cinematic preloader that plays automatically once on page load (~8 seconds total). A cream overlay covers the screen; inside it a giant 0 → 100% counter ticks up in randomized jumps while, below it, a stack of ten portrait images wipes across the frame — each image slides in from the left and, moments later, slides out to the right, producing a rapid slideshow-wipe. When the counter hits 100%, the digits slide up out of a mask, a giant masked wordmark slides up into the same slot and then slides up out again, the whole cream overlay fades away, the hero background image de-zooms from 2x to 1x, the giant hero name rises character-by-character out of its clip mask, and the top nav bar drops down into place. All motion is plain gsap.to tweens driven by power4.out / power3.inOut eases — no timeline object, no plugins.

Tech

Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) only — no GSAP plugins, no SplitText (text is split into <span>s by a tiny hand-written helper), no smooth-scroll library (the page does not scroll during the intro; it is a pure load-triggered sequence). Import with import gsap from "gsap";. Two independent trigger mechanisms run in parallel (details below): the image-stack tweens fire on fixed delay timers from load, while the counter/logo/overlay/hero/nav tweens are chained after a randomized counting loop completes.

Layout / HTML

Semantic structure (class names are load-bearing — the JS/CSS query them):

<nav>
  <div class="nav-logo"><a href="#">kudos</a></div>
  <div class="menu"><p>Menu</p></div>
  <div class="shop">
    <a href="#">Shop</a>
    <a href="#">Cart (0)</a>
  </div>
</nav>

<div class="hero">
  <img src="/hero.jpg" alt="" />
</div>

<div class="hero-copy">
  <h1>kudos</h1>
</div>

<div class="overlay">
  <div class="overlay-content">
    <div class="images">
      <div class="img-holder">
        <!-- exactly 10 identical <img> -->
        <img src="/loader.jpg" alt="" />
        <img src="/loader.jpg" alt="" />
        <img src="/loader.jpg" alt="" />
        <img src="/loader.jpg" alt="" />
        <img src="/loader.jpg" alt="" />
        <img src="/loader.jpg" alt="" />
        <img src="/loader.jpg" alt="" />
        <img src="/loader.jpg" alt="" />
        <img src="/loader.jpg" alt="" />
        <img src="/loader.jpg" alt="" />
      </div>
    </div>
    <div class="text">
      <div class="counter"><p>100%</p></div>
      <div class="logo"><p>kudos</p></div>
    </div>
  </div>
</div>

Notes:

  • Use "kudos" (lowercase) as the neutral placeholder brand/name — it appears in the nav logo, as the preloader wordmark (.logo p), and as the giant hero headline (.hero-copy h1).
  • The .img-holder holds exactly 10 <img> elements pointing at the same portrait image.
  • .overlay is a fixed full-screen cover with z-index:1000 that sits above everything during the intro; it is faded to opacity:0 at the end but is not removed from the DOM.
  • .counter p initial text is 100% (the JS overwrites it every counting tick).

Styling

Font (READ CAREFULLY — this is the #1 fidelity trap in this component):

  • The CSS declares html, body { width:100%; height:100%; font-family:"Manrope" }, but the original never actually loads Manrope — there is no <link> to Google Fonts, no @font-face, and no @import anywhere in the component. Because the only named family is unavailable and no generic fallback follows it, every browser falls back to its default serif (Times New Roman / a Didone-style display serif). That serif fallback is the real, rendered look you must reproduce — it is most visible on the star element.
  • So the giant preloader wordmark ("kudos"), the big counter digits, and the small nav text all render as a display serif: pronounced thick/thin stroke contrast, bracketed serifs, a Times-New-Roman-at-display-size / Didone-adjacent character — not a thin, even-weight sans.
  • Do NOT import Manrope, and do NOT swap in any sans-serif (Inter, Helvetica, Arial, etc.). Importing Manrope (or any sans) makes the giant wordmark and counter render thin and even-stroked, which visibly breaks the look of the hero element and is exactly the mistake to avoid here.
  • To reproduce faithfully: keep the literal font-family:"Manrope" declaration in the CSS (so the code matches the original) and load no font. For a deterministic serif across browsers you may append the generic keyword — font-family:"Manrope", serif — which lands on the same Times/serif fallback; if you instead add your own @font-face, pick a display serif with strong thick/thin contrast and serifs (Times New Roman, a Didone, Playfair-style) so it matches the original render. Never resolve the fallback to a sans-serif.

Palette:

  • --color-accent: #e6e0d8 (warm cream/oat — the overlay background, the nav background, and the color of the giant hero name)
  • #000000 (black — the counter digits and preloader wordmark, and nav text)

Global:

  • * { margin:0; padding:0; box-sizing:border-box }.
  • img { width:100%; height:100%; object-fit:cover }.

Key elements and their initial states (the animation depends on these exact values):

  • .overlay: position:fixed; top:0; left:0; width:100vw; height:100vh; background:var(--color-accent); display:flex; justify-content:center; align-items:center; z-index:1000.
  • .overlay-content: width:40%.
  • .images: position:relative; height:550px.
  • .img-holder: position:relative; width:80%; height:100%; margin:0 auto; z-index:2; clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%) (a full rectangle used purely as an overflow mask — the off-screen images are clipped by it).
  • .img-holder img: position:absolute; top:0; left:-110% (each image starts one-and-a-bit widths off to the left, hidden by the clip-path mask).
  • .text: position:relative; margin:1em 0; clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%) (again a rectangular mask clipping the counter/logo spans that slide above and below it).
  • .counter, .logo p: font-size:200px; text-align:center; text-transform:uppercase (renders in the default serif fallback described above — a display serif with thick/thin contrast, not a sans).
  • .counter p: line-height:100%.
  • .counter p span, .logo p span: position:relative; z-index:-2; color:#000000.
  • .logo: position:absolute; top:0; left:50%; transform:translateX(-50%) (overlaps the counter in the same masked slot).
  • .logo p: line-height:100%.
  • .logo p span: position:relative; top:200px (each wordmark char parked 200px below its natural line, hidden below the .text mask).
  • nav: position:fixed; top:-200px; width:100%; padding:1em; background-color:var(--color-accent); display:flex; align-items:center; z-index:1 (parked 200px above the top edge, out of view). nav > div { flex:1 }.
  • nav a, nav p: text-decoration:none; text-transform:uppercase; font-size:15px; color:#000; font-weight:500.
  • .menu { display:flex; justify-content:center }. .shop { display:flex; justify-content:flex-end; gap:2em }.
  • .hero: width:100vw; height:100vh.
  • .hero img: transform: scale(2) (starts zoomed to 2x; de-zooms to 1x).
  • .hero-copy: position:absolute; top:35%; left:50%; transform:translate(-50%,-50%); text-transform:uppercase; clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%) (rectangular mask over the giant name).
  • .hero-copy h1: font-weight:400; font-size:30vw; color:var(--color-accent); line-height:100%.
  • .hero-copy h1 span: position:relative; top:30vw (each char parked ~one line-height below, hidden inside the .hero-copy mask).

GSAP effect (be exact)

Text-split helper (not the SplitText plugin)

A hand-written function wraps every character of an element in a <span>:

function splitTextIntoSpans(selector) {
  const el = document.querySelector(selector);
  if (!el) return;
  el.innerHTML = el.innerText.split("").map((c) => `<span>${c}</span>`).join("");
}

On DOMContentLoaded, call it on .logo p and .hero-copy h1 so the preloader wordmark and the hero name each become a row of per-char spans (which the CSS above has already parked below their masks via top:200px / top:30vw).

Clock A — sliding image stack (fixed delay timers from load)

Registered inside the same DOMContentLoaded handler. Two plain tweens on the ten identical images:

A1 — images wipe IN (starts at delay: 4):

gsap.to(".img-holder img", {
  left: 0,          // from CSS left:-110%  → left:0 (fills the holder)
  stagger: 0.1,     // image 1 → 10, 0.1s apart
  ease: "power4.out",
  duration: 1.5,
  delay: 4,
});

Each image slides from off-left into the frame, one every 0.1s; because they stack in the same absolutely-positioned holder, the later images cover the earlier ones — a rapid left-to-right slideshow wipe. Span of this tween: ~4.0s → ~6.4s.

A2 — images wipe OUT (starts at delay: 7):

gsap.to(".img-holder img", {
  left: "110%",     // from left:0 → off to the right
  stagger: -0.1,    // NEGATIVE stagger: last image first, back to first
  ease: "power4.out",
  duration: 1.5,
  delay: 7,
});

The whole stack slides out to the right; the negative stagger reverses the order (image 10 leaves first). Span: ~7.0s → ~9.4s.

Clock B — randomized counter → chained reveal tweens

A startLoader() function runs immediately (called at module top level, before the DOMContentLoaded handler fires). It counts up in random jumps, then, once it reaches exactly 100, fires the reveal.

B0 — the counting loop (updateCounter, self-scheduling via setTimeout):

let currentValue = 0;
function updateCounter() {
  if (currentValue === 100) { animateText(); return; }
  currentValue += Math.floor(Math.random() * 10) + 1;   // jump by 1..10
  currentValue = Math.min(currentValue, 100);
  // re-render digits as spans + a "%" span, e.g. <span>7</span><span>3</span><span>%</span>
  document.querySelector(".counter p").innerHTML =
    currentValue.toString().split("").map((c) => `<span>${c}</span>`).join("") + "<span>%</span>";
  setTimeout(updateCounter, Math.floor(Math.random() * 200) + 100); // 100..300ms between ticks
}
updateCounter();

The big black number climbs from 0% to 100% in irregular jumps, each digit re-wrapped in its own span every frame. With avg jump ~5.5 and avg delay ~200ms it lands on 100 at roughly 3–3.5s — so it finishes right as the image stack (Clock A) is mid-wipe. (The two clocks are independent; the counter's landing time is random and is tuned only by these ranges.)

B1 — the reveal (animateText, called when the count hits 100). It waits setTimeout(..., 300) and then fires seven independent gsap.to tweens. Treat t = 0 as the moment these tweens are created (≈ count-finish + 0.3s). All use ease:"power3.inOut".

// 1) counter digits slide UP out of the mask
gsap.to(".counter p span", { top: "-400px", stagger: 0.1, ease: "power3.inOut", duration: 1 });          // t=0

// 2) wordmark chars slide UP INTO the mask (top:200px → 0)
gsap.to(".logo p span",     { top: "0",      stagger: 0.1, ease: "power3.inOut", duration: 1 });          // t=0

// 3) wordmark chars slide UP out of the mask (0 → -400px)
gsap.to(".logo p span",     { top: "-400px", stagger: 0.1, ease: "power3.inOut", duration: 1, delay: 3 });// t=3

// 4) cream overlay fades away (opacity 1 → 0; NOT removed from DOM)
gsap.to(".overlay",         { opacity: 0,                  ease: "power3.inOut", duration: 1, delay: 4 }); // t=4

// 5) hero image de-zooms scale 2 → 1
gsap.to(".hero img",        { scale: 1,                    ease: "power3.inOut", duration: 2, delay: 3.5 });// t=3.5

// 6) giant hero name rises char-by-char (top:30vw → 0)
gsap.to(".hero-copy h1 span",{ top: "0",     stagger: 0.1, ease: "power3.inOut", duration: 2, delay: 4 }); // t=4

// 7) nav bar drops down (top:-200px → 0)
gsap.to("nav",              { top: "0",                    ease: "power3.inOut", duration: 2, delay: 4 }); // t=4

Sequence within Clock B, in words:

  1. t=0–1: as the counter reaches 100, its digits slide straight up and out of the .text mask while, in the same slot, the "kudos" wordmark rises up into view (a swap: number out, name in).
  2. t=3–4: the wordmark slides up and out of the mask (clears the slot).
  3. t=3.5–5.5: the hero background image de-zooms from 2x to 1x.
  4. t=4–5: the cream overlay fades to transparent, revealing the hero underneath.
  5. t=4–6: the giant cream hero name rises character-by-character out of its clip mask (top:30vw → 0, staggered 0.1).
  6. t=4–6: the top nav bar slides down from above into place.
Timeline summary (approximate absolute seconds, counter landing ≈ 3.3s)

| t (s) | what | |------|------| | 0–3.3 | counter jumps 0%→100% (random), black digits re-rendered each tick | | ~4.0–6.4 | image stack wipes IN, left→right (left:-110%→0, stagger 0.1, power4.out, 1.5s) | | ~3.6 | count done → 300ms pause → reveal tweens created | | ~3.6–4.6 | digits slide up out (-400px) + wordmark rises in (200px→0) | | ~6.6–7.6 | wordmark slides up out (0→-400px) | | ~7.1–9.1 | hero image de-zooms 2→1 | | ~7.6–8.6 | overlay fades opacity 1→0 | | ~7.6–9.6 | hero name rises char-by-char (30vw→0, stagger 0.1) + nav drops (-200px→0) | | ~7.0–9.4 | image stack wipes OUT to the right (0→110%, negative stagger) |

Total runtime ≈ 8 seconds (preview waits ~8.5s). Because the counter is randomized, exact overlaps drift a little each load — that variability is intentional. The fully-revealed final state (overlay gone, hero de-zoomed, giant hero name up, nav dropped in) only settles at the very end of the sequence (~9.6s); a faithful side-by-side should be captured near the end so both renders show the same phase rather than one mid-reveal.

Ease / value reference
  • Image stack in/out: ease:"power4.out", duration:1.5, stagger:0.1 (in) / -0.1 (out).
  • Every reveal tween (digits, wordmark, overlay, hero image, hero name, nav): ease:"power3.inOut".
  • Reveal durations: counter/wordmark = 1, hero image / hero name / nav = 2, overlay = 1.
  • Reveal staggers (all char groups): 0.1.
  • The masks are CSS clip-path rectangles on .img-holder, .text, and .hero-copy — nothing masks via overflow; the animated left / top offsets slide content past those clip edges.

Assets / images

  • 1 hero image (hero.jpg): a full-bleed landscape (~16:9 / ~1.9:1) photograph used as the whole-viewport hero background (object-fit:cover, initially scale(2), de-zooming to 1). Warm, naturally-lit editorial lifestyle scene — e.g. two people laughing together at a laptop in a cozy sunlit home/office, soft warm neutrals. Any warm-toned, bright lifestyle/editorial full-bleed image works.
  • 10 preloader images (loader.jpg, repeated 10×): a portrait (~2:3, ~0.69:1) photograph filling the .img-holder (object-fit:cover). Warm, naturally-lit lifestyle portrait matching the hero's palette — e.g. a person in a bright, plant-filled home workspace. The ten can be the same image (identical, as in the original) or a small matched set; because they overlap and wipe fast, a single repeated portrait reads as a clean recurring wipe.

No client brands anywhere — use "kudos" as the neutral wordmark.

Behavior notes

  • Autoplays once on load; no scroll, hover, or click triggers. The page does not scroll during the intro. startLoader() runs at module top level; the splitTextIntoSpans calls and the image-stack tweens run in the DOMContentLoaded handler (which fires after the module executes, so the wordmark spans exist before animateText needs them).
  • The .overlay stays in the DOM at opacity:0 after the reveal (it is faded, not removed). Give the hero content its natural stacking so the transparent overlay does not block interaction if you extend the page.
  • No reduced-motion or responsive re-timing in the original — a prefers-reduced-motion guard that skips straight to the final state (overlay hidden, hero scale:1, name/nav in place) is a reasonable add.
  • Responsive (@media max-width: 900px): .overlay-content widens to 75%; .counter, .logo p font-size drops from 200px to 100px. The animation values are otherwise unchanged.