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:
TheandQuiet Crowd. .emojisstarts 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. Initialclip-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'sclip-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 === " " ? " " : c}</span>`)
.join("");
});
}
splitTextIntoSpans(".header h1");
Each glyph becomes its own <span> (spaces → two ). 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 tweeny: 200px → 0over 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 position0, so "The" and "Quiet Crowd" reveal simultaneously. The rowclip-pathmasks each glyph until it arrives. - Medallion pop-in:
.hero-imgscales0 → 1over 1.5s,power4.out, at position0. - Medallion endless spin: simultaneously (also position
0).hero-imgtweensrotation: 0 → 360over 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):
- Pick a random
sizefrom[150,200,250,300]and a randomvariantin1..4. - Create
<div class="emoji">; set inlinewidth/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. - Pick
initialRotationrandomly from[90, -90]. - Compute a pacing delay so rapid spawns don't all drop at once:
delayFromLast = Math.max(0, emojiFallDelay - (Date.now() - lastEmojiTime)) / 1000 (seconds).
gsap.set(emoji, { scale: 0, rotation: initialRotation })— starts invisible and tilted.- 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 0° (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.
- Return
Date.now()(stored aslastEmojiTimefor 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.png— 4 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
mousemoveand stops permanently onceisLoadingflips tofalseat the start of the hero reveal. No touch handling (not mobile-safe). .emojisoverlay ispointer-events:noneandoverflow:hidden— particles never block interaction and are clipped to the viewport; each self-removes after falling off-screen.- Keep the
will-changehints (clip-pathon.preloader,transformon.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).