All components

Responsive Landing Page Reveal

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

Open live demo ↗ Raw prompt (.md)

What it does

On page load a full-screen preloader plays: a numeric counter races from 0 to 100 while an anime.js easeOutExpo timeline reveals the label letter-by-letter and slides it away, then GSAP power4.inOut tweens scale the loader down and wipe the black and red panels (animating height to 0) to uncover the hero GIF via clip-path, staggering in the headline (gsap.from y) and footer images. Entirely time-delayed on load with no user interaction.

How it's built

Categorypreloader
Techgsap
Complexitypage
Performance costlight
Mobile-safeyes

preloader reveal counter clip-path gsap anime.js staggered landing editorial

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

Full-Screen Preloader → Curtain-Wipe Landing Reveal (counter + layered clip-path wipe)

Goal

Build an auto-playing landing-page preloader for a fictional design studio called New Reality. On load a full-screen black panel fills the viewport; a numeric counter races 0 → 100 in the center while an editorial label ("NEW REALITY") reveals letter-by-letter from above, holds, then slides letter-by-letter downward out of view. At ~3 s the whole preloader scales down to 0.5 and then, in a tightly staggered sequence, three stacked full-screen layers each collapse upward — a black panel and a red panel animate their height to 0, and a hero-image layer wipes away via clip-path — peeling back like curtains to uncover the site underneath: a giant 20vw headline whose seven letters rise up into place, and two footer thumbnails that wipe open from the left. Everything is purely time-delayed on page load — no scroll, hover, or click.

Tech

Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) only — no GSAP plugins, no ScrollTrigger, no Lenis/smooth-scroll. The page never scrolls; every animation is a load-triggered gsap.to / gsap.from with a delay.

import gsap from "gsap";

Two effects are not GSAP and must be reproduced as described:

  1. The counter (0 → 100) is a plain-JS setTimeout loop with random increments — not a tween.
  2. The label letter reveal was originally an anime.js v3.2.2 timeline using easeOutExpo. Reproduce it with GSAP: anime's easeOutExpo is mathematically identical to GSAP's expo.out (1 − 2^(−10·t)), so two gsap.fromTo tweens reproduce it faithfully with a single dependency (see the GSAP section). Do not add anime.js.

All animation code runs immediately on module execution (no DOMContentLoaded wrapper is required).

Layout / HTML

Two sibling blocks: a .container holding the preloader layers, and a .site-content block (the real page) that sits behind everything (z-index: -2). Class/tag names are load-bearing — the JS and CSS query them exactly.

<div class="container">
  <div class="pre-loader">
    <div class="loader"></div>       <!-- opaque BLACK full-screen panel -->
    <div class="loader-bg"></div>    <!-- RED full-screen panel, behind .loader -->
  </div>

  <div class="loader-content">       <!-- centered counter + label, on top -->
    <div class="count"><p>0</p></div>
    <div class="copy"><p class="ml16">New reality</p></div>
  </div>

  <div class="loader-2"></div>       <!-- full-screen HERO IMAGE layer, behind the panels -->
</div>

<div class="site-content">
  <nav>
    <div class="logo"><a href="#">New Reality</a></div>
    <div class="links">
      <a href="#">Info</a>
      <a href="#">Portfolio</a>
      <a href="#">Contact</a>
    </div>
  </nav>

  <div class="header">   <!-- one <h1> per letter → spells "NEWREAL" across the viewport -->
    <h1>N</h1><h1>e</h1><h1>w</h1><h1>r</h1><h1>e</h1><h1>a</h1><h1>l</h1>
  </div>

  <footer>
    <div class="footer-copy">
      <p>New Reality is a design studio based in Tokyo, Japan. We work with many
         companies to build and proactively deliver engaging brand experiences. We are
         unique in our ability to take a strategic approach while being visually driven.</p>
    </div>
    <div class="footer-nav">
      <div class="img"></div>   <!-- footer thumbnail 1 (CSS background-image) -->
      <div class="img"></div>   <!-- footer thumbnail 2 (CSS background-image) -->
    </div>
  </footer>
</div>

Notes:

  • The seven .header h1 each contain one letter; with text-transform: uppercase they read NEWREAL spread across the full width, each letter in its own equal-flex column.
  • The two .img divs get their images from CSS background-image (not <img> tags).

Styling

Global reset: * { margin:0; padding:0; box-sizing:border-box }. html, body { width:100vw; height:100vh; font-family:"Neue Montreal" }.

Palette (literal values):

  • Loader panel: black #000, text white #fff.
  • Second panel .loader-bg: red (#ff0000).
  • Site text / links: black #000 on the page's default (transparent → white) background.

Typography:

  • Body: "Neue Montreal" (a neutral grotesque sans; fall back to a clean sans-serif).
  • .copy label and .header h1: "PP Editorial Old" (a high-contrast editorial serif; fall back to a serif). These are custom fonts and are simply referenced by name — load them via CDN/@font-face if available, otherwise the serif/sans fallbacks are fine.
  • .copy: font-size: 30px; text-transform: uppercase; line-height: 1.
  • .header h1: font-size: 20vw; font-weight: 500; text-transform: uppercase; line-height: 1; text-align: center.

Key positioning & the states the animation depends on (initial values matter):

  • .pre-loaderposition: fixed; top:0; width:100%; height:100%; clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%) (a full-rect clip; present so the panel is a clean fixed layer).
  • .loaderposition: absolute; top:0; width:100%; height:100%; background:#000; color:#fff; display:flex; justify-content:center; align-items:center (opaque black, full screen; starts at height:100%).
  • .loader-bgposition: absolute; top:0; width:100%; height:100%; background:red; z-index:-1 (red panel sitting behind the black one; starts at height:100%).
  • .loader-contentposition: absolute; top:50%; left:50%; transform: translate(-50%,-50%); display:flex; width:400px; z-index:2; color:#fff (the centered counter+label, on top of everything).
  • .countflex: 2; text-align: right; line-height: 1; padding: 0 1em.
  • .copyflex: 6 (the serif label, styled as above).
  • .ml16overflow: hidden (this masks the letters as they slide in/out).
  • .ml16 .letterdisplay: inline-block; line-height: 1em (JS wraps each non-space character in <span class="letter">).
  • .loader-2position: absolute; top:0; width:100%; height:100%; background: url(<hero image>) no-repeat 50% 50%; background-size: cover; z-index:-1; clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%) (the hero-image layer, starts as a full-rect clip = fully visible, sitting behind the red/black panels).
  • .site-contentposition: relative; z-index: -2 (the real page, behind all preloader layers).
  • navwidth:100%; padding:2em; display:flex. nav > div { flex:1 }. .links { display:flex; justify-content:flex-end; gap:5em }. a { text-decoration:none; color:#000 }.
  • .headerdisplay:flex; padding:1em. .header h1 { flex:1; position:relative } (see typography). No overflow:hidden here.
  • footerposition: fixed; bottom:0; width:100%; display:flex; align-items:flex-end; padding:2em. footer > div { flex:1 }. .footer-copy p { width:50% }. .footer-nav { display:flex; gap:2em; justify-content:flex-end }.
  • .imgwidth:225px; height:150px; background-size:cover; background-position:50% 50% and clip-path: polygon(0% 0%, 0% 0%, 0% 100%, 0% 100%) (a zero-width sliver pinned to the left edge → the image is initially hidden and reveals by wiping open rightward). Assign each a background image (.img:nth-child(1), .img:nth-child(2)).

GSAP effect (be exhaustive)

Fire everything at module load. There is no master timeline — these are independent gsap tweens plus one plain-JS counter and a two-step letter animation. Times below are seconds from load.

1. Counter 0 → 100 (plain JS, not GSAP)
function startLoader() {
  const el = document.querySelector(".count p");
  let value = 0;
  (function tick() {
    if (value < 100) {
      value = Math.min(value + (Math.floor(Math.random() * 10) + 1), 100); // +1..+10
      el.textContent = value;
      setTimeout(tick, Math.floor(Math.random() * 200) + 25);              // 25..225 ms
    }
  })();
}
startLoader();

The number jumps up in random steps of 1–10 with random 25–225 ms gaps, hitting 100 around ~2–3 s. It is then faded out by GSAP (steps 2 & 5 below).

2. Label letters — reveal, hold, exit (GSAP expo.out, reproducing the anime.js easeOutExpo timeline)

First wrap each visible character of .ml16 in a span, then run two fromTo tweens:

const wrap = document.querySelector(".ml16");
wrap.innerHTML = wrap.textContent.replace(/\S/g, "<span class='letter'>$&</span>");
const letters = document.querySelectorAll(".ml16 .letter"); // "Newreality" → 10 letters (space not wrapped)

// Phase 1 — reveal: each letter drops in from 100px above to 0
gsap.fromTo(letters,
  { y: -100 },
  { y: 0, ease: "expo.out", duration: 1.5, stagger: 0.03 }   // stagger 30ms per letter, from load
);

// Phase 2 — exit: each letter slides 100px downward out of view
gsap.fromTo(letters,
  { y: 0 },
  { y: 100, ease: "expo.out", duration: 3, stagger: 0.03, delay: 3.77 }
);

Timing rationale (matching the original anime.js timeline): phase 1 is translateY [-100 → 0], 1.5 s, easeOutExpo, per-letter delay 30·i ms. Phase 2 is appended after phase 1 ends (~1.77 s) with an additional per-letter delay of 2000 + 30·i ms → each letter begins its downward exit at ≈ 3.77 s + 30 ms·i, animating translateY [0 → 100] over 3 s. The overflow:hidden on .ml16 masks the letters through both phases.

3. Counter fade (early)
gsap.to(".count", { opacity: 0, delay: 3.5, duration: 0.25 });
4. Preloader scale-down
gsap.to(".pre-loader", { scale: 0.5, ease: "power4.inOut", duration: 2, delay: 3 });

The entire preloader (black + red panels) shrinks from scale 1 → 0.5 toward the viewport center, starting at 3 s over 2 s.

5. Counter fade (final, redundant with #3)
gsap.to(".count", { opacity: 0, ease: "power2.inOut", duration: 0.5, delay: 3.75 });
6. Black panel collapses upward
gsap.to(".loader", { height: "0", ease: "power4.inOut", duration: 1.5, delay: 3.75 });

.loader is top:0, so animating height 100% → 0 collapses the black panel upward.

7. Red panel collapses upward
gsap.to(".loader-bg", { height: "0", ease: "power4.inOut", duration: 1.5, delay: 4 });

Same collapse for the red panel, 0.25 s behind the black one — so you glimpse the red layer as the black one clears.

8. Hero-image layer wipes away (clip-path)
gsap.to(".loader-2", {
  clipPath: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)",
  ease: "power4.inOut", duration: 1.5, delay: 3.5,
});

The hero-image layer's clip goes from a full rectangle (0 0, 100% 0, 100% 100%, 0 100%) to a zero-height line at the top (both bottom corners pulled up to y:0) — the image collapses/wipes upward and disappears, exposing the .site-content behind it. Because the black (#6) and red (#7) panels are collapsing at almost the same moment, the three layers peel back like stacked curtains.

9. Headline letters rise in
gsap.from(".header h1", { y: 200, ease: "power4.inOut", duration: 1.5, delay: 3.75, stagger: 0.05 });

The seven NEWREAL letters each animate from y:200 (200px below) up to their resting position, staggered 0.05 s.

10. Footer thumbnails wipe open
gsap.to(".img", {
  clipPath: "polygon(0 0, 100% 0, 100% 100%, 0 100%)",
  ease: "power4.inOut", duration: 1.5, delay: 4.5, stagger: 0.25,
});

Each .img clip animates from its CSS zero-width left sliver to a full rectangle → the two thumbnails wipe open left-to-right, 0.25 s apart.

Approximate timeline (seconds from load)

| t | event | |---|-------| | 0 | counter starts ticking; label letters reveal from above (phase 1) | | ~2–3 | counter reaches 100 | | 3.0 | preloader begins scaling 1 → 0.5 (#4) | | 3.5 | counter fade #3; hero-image layer begins clip-wipe up (#8) | | 3.75 | final counter fade (#5); black panel height→0 (#6); headline letters rise (#9); label letters begin downward exit (phase 2) | | 4.0 | red panel height→0 (#7) | | 4.5 | footer thumbnails begin wiping open (#10) | | ~5.5–6.5 | all reveals settle; site fully visible |

Total intro ≈ 6.5 s.

Assets / images

  • 1 hero background image on .loader-2, full-bleed (background-size: cover, landscape ~16:9). A warm, richly-toned editorial/artwork image — it flashes into view mid-transition as the panels peel back, then wipes upward. Any bold, warm full-bleed image works in this role; it is only visible for a moment during the curtain wipe.
  • 2 footer thumbnails on the .img divs, each rendered 225×150 px (~3:2 landscape), background-size: cover. Editorial/photographic in tone (e.g. a motion-blurred warm-monochrome crowd, and a moody hand-holding-glass-crystal shot). They reveal by clip-path wiping open from the left. Cropping via cover is expected.

No client branding — the demo studio name is "New Reality" ("Newreal" in the split headline). Keep all copy neutral/placeholder.

Behavior notes

  • Autoplay once on load; no scroll, hover, or click anywhere. The page itself does not scroll.
  • Keep the layer stacking exact: .loader-content z-index:2 (top) → .loader (black) → .loader-bg (red, z-index:-1) → .loader-2 (hero, z-index:-1) → .site-content (z-index:-2, bottom). The reveal reads correctly only with this order.
  • The overflow:hidden on .ml16 and the exact initial clip-path values on .loader-2 and .img are essential — they are what the tweens animate away from.
  • Responsive @media (max-width: 900px): footer becomes flex-direction: column; gap: 2em, .footer-copy p widens to width:100%, and .footer-nav becomes width:100%; justify-content:space-between. The animation logic is unchanged.
  • No reduced-motion handling exists in the original; add a prefers-reduced-motion guard if desired, but it is not part of the reference behavior.