All components

The Quiet Crowd Landing Reveal

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

Open live demo ↗ Raw prompt (.md)

What it does

On page load a GSAP timeline wipes a clip-path preloader and spins a loader, then reveals the hero: heading characters (manually split into spans) slide up with a staggered power4.out tween while a circular hero image scales in and rotates infinitely. Moving the cursor during the load spawns emoji particles that pop in with a back.out ease and drop off-screen.

How it's built

Categorypreloader
Techgsap
Complexitypage
Performance costlight
Mobile-safedesktop-first

preloader hero-reveal gsap text-reveal cursor-trail emoji clip-path playful landing-page

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 Preloader + Hero Reveal with Cursor Emoji Confetti

Goal

Build a full-screen playful/editorial landing intro. A neutral beige preloader holds a small spinning square loader for a few seconds, then wipes upward off the top of the screen to reveal a bright-yellow hero. As it reveals, the two-line headline animates in character by character (each glyph slides up from below its clip mask), and a circular graphic medallion pops in at screen center and starts an endless slow rotation. The signature detail: while the preloader is still running, moving the cursor sprinkles round emoji "stickers" at the pointer — each pops in with a bouncy back.out ease, hangs for a beat, then drops off the bottom of the screen and unmounts. All motion is GSAP core; text is split into <span>s manually (no SplitText plugin).

Tech

Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) only — no plugins (no ScrollTrigger, no SplitText, no CustomEase) and no smooth-scroll library. The page does not scroll; it is a pure load-triggered sequence plus a mousemove particle spawner. import gsap from "gsap"; at the top of the module. Everything runs immediately on module load (script is type="module", placed at end of <body>).

Layout / HTML

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

<div class="container">
  <div class="preloader">
    <div class="loader"></div>
  </div>

  <div class="emojis"></div>   <!-- empty; runtime particles are appended here -->

  <section class="hero">
    <div class="nav">
      <div class="site-info">
        <p>Welcome to The Quiet Crowd! This is your gateway to a world of
           refined branding, where elegance meets ingenuity. Step into a realm
           where your brand's uniqueness whispers louder than a shout,
           captivating the hearts of your audience. The Quiet Crowd doesn't
           just create brands—it curates timeless identities that resonate.</p>
      </div>
      <div class="logo"><img src="/assets/logo.png" alt="" /></div>
      <div class="menu-btn"><img src="/assets/menu-btn.png" alt="" /></div>
    </div>

    <div class="hero-img"><img src="/assets/hero.png" alt="" /></div>

    <div class="header">
      <div class="header-row"><h1>The</h1></div>
      <div class="header-row"><h1>Quiet Crowd</h1></div>
    </div>
  </section>
</div>

Notes:

  • Use "The Quiet Crowd" as the neutral placeholder brand. Headline is exactly two rows: The and Quiet Crowd.
  • .emojis starts empty — the JS creates <div class="emoji"> particles inside it at runtime.
  • The nav is a 3-column row: welcome paragraph (left), logo (center), menu button (right).

Styling

Fonts: body is "Inter" (import from Google Fonts, weight 400). Headline h1 declares font-family: "Cy Grotesk" — this is an unavailable custom light display face; keep the declaration but let it fall back to a light-weight grotesque/sans (font-weight: lighter). Don't attempt to load the missing font.

Palette (exact hex — this is a high-contrast, offbeat scheme):

  • Preloader background #ded7ce (warm beige); loader square #c5beb5 (muted taupe).
  • Hero background #faec6c (bright lemon yellow).
  • Emoji chip background #e3e3e3 (light grey — shows behind transparent stickers).
  • Text is default black.

Global:

  • * { margin:0; padding:0; box-sizing:border-box }.
  • img { width:100%; height:100%; object-fit:cover }.
  • html, body { width:100%; height:100%; font-family:"Inter" }.
  • Custom cursor: html, body { cursor: url("/assets/cursor.svg") 32 32, auto } — a custom circular cursor graphic with a 32×32 hotspot.
  • .container { width:100%; height:100% }.

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

  • .preloader: position:fixed; inset:0; width:100vw; height:100vh; background:#ded7ce; display:flex; justify-content:center; align-items:center; overflow:hidden; z-index:1. Initial clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%) (full rectangle, covering everything). will-change:clip-path.
  • .loader: position:relative; width:40px; height:40px; background:#c5beb5; will-change:transform (a small solid square, centered in the preloader).
  • .emojis: position:fixed; inset:0; width:100%; height:100%; overflow:hidden; pointer-events:none; z-index:2 (a full-screen clipping overlay above everything; never intercepts the pointer).
  • .emoji (runtime particle): position:absolute; background:#e3e3e3; border-radius:100%; overflow:hidden; pointer-events:none; will-change:transform; background-repeat:no-repeat; background-position:50% 50%; background-size:cover (a circular chip whose sticker image cover-fills it; width/height/left/top/background-image are set inline per-particle).
  • .hero: position:relative; width:100vw; height:100vh; overflow:hidden; background:#faec6c; display:flex; flex-direction:column; justify-content:space-between; z-index:0 (nav pinned to top, header pinned to bottom).
  • .nav: width:100vw; display:flex; align-items:flex-start. .nav > div { flex:1; padding:1.5em } (three equal columns).
  • .site-info p: width:450px; font-size:18px; font-weight:400.
  • .logo: display:flex; justify-content:center. .logo img { width:200px; object-fit:contain; transform:scale(0.5) } (rendered ~100px, centered).
  • .menu-btn: display:flex; justify-content:flex-end. .menu-btn img { width:70px; object-fit:contain } (top-right).
  • .hero-img: position:absolute; top:50%; left:50%; transform:translate(-50%,-50%) scale(0); width:40%; aspect-ratio:1; transform-origin:center; will-change:transform (a square medallion at exact screen center, starts at scale 0 = invisible).
  • .header: width:100%; display:flex; flex-direction:column; gap:0.75em; padding:1em 2em (the two rows, bottom-left of hero).
  • .header-row: position:relative; padding-top:0.5em; clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%) — a full-rect clip mask so the below-parked glyphs are hidden until they slide up into the row.
  • .header h1: font-family:"Cy Grotesk"; font-size:9.5vw; font-weight:lighter; line-height:90% (huge, thin).
  • .header h1 span: position:relative; display:inline-block; transform:translateY(200px)every character is parked 200px below its baseline, clipped by the row's clip-path, waiting to slide up.

GSAP effect (be exact)

Setup — manual character split (no SplitText)

Before any animation, run a helper that splits both headline h1s into per-character <span>s:

function splitTextIntoSpans(selector) {
  document.querySelectorAll(selector).forEach((el) => {
    el.innerHTML = el.innerText
      .split("")
      .map((c) => `<span>${c === " " ? "&nbsp;&nbsp;" : c}</span>`)
      .join("");
  });
}
splitTextIntoSpans(".header h1");

Each glyph becomes its own <span> (spaces → two &nbsp;). Combined with the CSS .header h1 span { transform: translateY(200px) }, every character starts pushed 200px down and clipped by its row mask.

Preloader phase — three independent load tweens (fire immediately)

1 — Preloader wipe (delay: 5):

gsap.to(".preloader", {
  clipPath: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)",
  duration: 1.5,
  delay: 5,
  ease: "power4.inOut",
});

After a 5s hold, the preloader's clip-path collapses its two bottom corners up to the top edge — the whole beige panel wipes upward off the top over 1.5s (revealing the yellow hero beneath). Runs from t≈5.0 → t≈6.5.

2 — Loader spin, then shrink (delay: 1):

gsap.to(".loader", {
  rotation: "+=180",
  duration: 1.5,
  delay: 1,
  repeat: 1,           // plays twice → two 180° half-turns, 3s total
  ease: "power4.inOut",
  onComplete: () => {
    gsap.to(".loader", {
      scale: 0,
      duration: 2,
      ease: "power4.inOut",
      onComplete: initializePageAnimations,  // hands off to the hero reveal
    });
  },
});

The small square starts spinning at t=1: +=180° with power4.inOut, repeat:1 so it does the half-turn twice (3s of rotation, ends ~t=4). Then it scales 1 → 0 over 2s (ends ~t=6) and calls initializePageAnimations. So the hero reveal (below) kicks off at t≈6, overlapping the tail of the preloader wipe.

3 — Emoji spawning gate: a module-level let isLoading = true; is set to false as the very first line of initializePageAnimations. The mousemove particle spawner (below) only runs while isLoading is true — i.e. the emoji confetti is a preloader-only interaction and stops the moment the hero reveal begins.

Hero reveal — initializePageAnimations() (one timeline, everything at position 0)
function initializePageAnimations() {
  isLoading = false;
  const tl = gsap.timeline();

  // headline: each row's chars slide up into view, both rows in parallel
  document.querySelectorAll(".header-row").forEach((row) => {
    tl.to(row.querySelectorAll("span"), {
      y: 0,
      duration: 1,
      ease: "power4.out",
      stagger: { amount: 0.25, from: "start" },
    }, 0);
  });

  // center medallion pops in AND begins an endless spin, both at position 0
  tl.to(".hero-img", { scale: 1, duration: 1.5, ease: "power4.out" }, 0)
    .to(".hero-img", { rotation: 360, duration: 20, ease: "none", repeat: -1 }, 0);
}
  • Headline reveal: for each .header-row, its character <span>s tween y: 200px → 0 over 1s, power4.out, stagger { amount: 0.25, from: "start" } (the 0.25s of stagger is distributed across that row's glyphs, left to right). Both rows are added at position 0, so "The" and "Quiet Crowd" reveal simultaneously. The row clip-path masks each glyph until it arrives.
  • Medallion pop-in: .hero-img scales 0 → 1 over 1.5s, power4.out, at position 0.
  • Medallion endless spin: simultaneously (also position 0) .hero-img tweens rotation: 0 → 360 over 20s, ease:"none" (linear), repeat:-1 (infinite) — a slow, perpetual clockwise rotation that continues forever after the reveal settles.
Cursor emoji confetti — mousemove spawner (only while isLoading)

Module constants and state:

const mouseDistance   = 400;                 // px of travel needed to spawn
const emojiWaitTime   = 500;                 // ms a particle hangs before dropping
const emojiFallDelay  = 200;                 // ms min gap that paces sequential drops
const emojiRotations  = [90, -90];           // random initial/exit tilt
const emojiSizes      = [150, 200, 250, 300];// random chip diameter (px)
const totalEmojiVariants = 4;                // emoji-1..4
let lastMouseX = 0, lastMouseY = 0, lastEmojiTime = 0;

Listener:

document.addEventListener("mousemove", (e) => {
  if (!isLoading) return;                     // preloader-phase only
  const d = Math.hypot(e.clientX - lastMouseX, e.clientY - lastMouseY);
  if (d > mouseDistance) {                     // spawn once per 400px of travel
    lastEmojiTime = createEmoji(e.clientX, e.clientY);
    lastMouseX = e.clientX;
    lastMouseY = e.clientY;
  }
});

Every time the cursor has travelled more than 400px from the last spawn point, drop a new emoji at the current pointer position.

createEmoji(mouseX, mouseY):

  1. Pick a random size from [150,200,250,300] and a random variant in 1..4.
  2. Create <div class="emoji">; set inline width/height = sizepx, backgroundImage = url(/assets/emoji-${variant}.png), and position it centered on the cursor: left = mouseX - size/2, top = mouseY - size/2 (px). Append to .emojis.
  3. Pick initialRotation randomly from [90, -90].
  4. Compute a pacing delay so rapid spawns don't all drop at once:

delayFromLast = Math.max(0, emojiFallDelay - (Date.now() - lastEmojiTime)) / 1000 (seconds).

  1. gsap.set(emoji, { scale: 0, rotation: initialRotation }) — starts invisible and tilted.
  2. Two-step timeline:

``js gsap.timeline() .to(emoji, { // POP IN scale: 1, rotation: 0, duration: 0.5, ease: "back.out(1.75)", // bouncy overshoot }) .to(emoji, { // HANG, THEN DROP y: window.innerHeight + size, // falls fully off the bottom rotation: initialRotation, // tilts back as it falls duration: 0.5, ease: "power2.in", // accelerating fall delay: emojiWaitTime / 1000 + delayFromLast, // 0.5s + pacing onComplete: () => emoji.remove(),// unmount when off-screen }); ` So each chip springs up from scale 0 to 1 while un-tilting to (back.out(1.75), 0.5s), hangs ~0.5s (plus pacing), then falls straight down past the viewport bottom (y: innerHeight + size) while re-tilting to its initialRotation, accelerating (power2.in`), and removes itself on completion.

  1. Return Date.now() (stored as lastEmojiTime for the pacing math).
Timeline summary (approximate seconds)

| t (s) | what | |------|------| | 0–1 | idle; cursor movement spawns emoji confetti (pop-in back.out, hang, drop) | | 1–4 | loader square spins +=180° twice (power4.inOut) | | 4–6 | loader scales 1 → 0 (power4.inOut) | | 5–6.5 | preloader clip-path wipes upward off the top (power4.inOut) | | ~6 | isLoading=false (emojis stop); hero reveal timeline starts | | 6–7 | headline chars slide up y:200→0, both rows, stagger 0.25, power4.out | | 6–7.5 | center medallion scales 0 → 1 (power4.out) | | 6 →∞ | medallion rotates 0 → 360 linearly, repeat:-1 (endless) |

Ease reference
  • Preloader wipe, loader spin, loader shrink: power4.inOut.
  • Headline char reveal and medallion scale-in: power4.out.
  • Medallion infinite spin: none (linear).
  • Emoji pop-in: back.out(1.75); emoji fall: power2.in.

Assets / images

All PNGs on transparent (or square) backgrounds; the emoji stickers are cover-cropped inside circular chips, everything else is object-fit:contain/cover as noted.

  • hero.png — 1 square (1:1) graphic used as the central rotating medallion. In the reference it is an iridescent green/cyan wireframe globe (thin gridlines, transparent interior). Any bold, roughly-symmetrical square graphic reads well spinning at screen center.
  • logo.png — 1 small wide (~2:1) brand lockup illustration for the centered nav logo. Reference: a flat editorial illustration of a figure in a trench coat and glasses pointing. Any compact horizontal wordmark/illustration works.
  • menu-btn.png — 1 small square icon for the top-right menu button. Reference: a glossy 3D inflated balloon letter. Any playful square icon works.
  • emoji-1.png … emoji-4.png4 square (1:1) sticker graphics, a deliberately mixed set of playful 3D and flat illustrations (e.g. a chrome star, a holographic blob, flat-illustrated characters). Each is displayed cover-filling a circular chip, so any square image works; they're spawned at random.
  • cursor.svg — 1 custom circular cursor graphic (~48px, 32×32 hotspot) replacing the default pointer site-wide.

If fewer than 4 emoji stickers are available, repeat to fill the set.

Behavior notes

  • Autoplay once on load; total intro ≈ 6.5s to the hero, then the medallion spins forever. No scroll, no click triggers.
  • Emoji confetti is preloader-only and desktop/pointer-only — it requires mousemove and stops permanently once isLoading flips to false at the start of the hero reveal. No touch handling (not mobile-safe).
  • .emojis overlay is pointer-events:none and overflow:hidden — particles never block interaction and are clipped to the viewport; each self-removes after falling off-screen.
  • Keep the will-change hints (clip-path on .preloader, transform on .loader, .hero-img, and .emoji) — they matter for smooth clip-path and transform animation.
  • No console errors expected on load; the sequence self-starts and needs no interaction to complete (interaction only adds the emoji layer).