All components

P10 Landing Page Reveal

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

Open live demo ↗ Raw prompt (.md)

What it does

A preloader-driven landing reveal that plays automatically on load: SplitText masked characters slide the studio name and a large '10' into view, the intro character slides and scales into a logo mark while animated corner tags flip in and out, then two dark overlays split apart along a clip-path seam to unveil a full-bleed hero image, a center card and a masked per-character card heading. Driven by a single GSAP timeline using a custom 'hop' CustomEase and staggered clip-path/transform tweens.

How it's built

Categorypreloader
Techgsap
GSAP pluginsCustomEase, SplitText
Complexitypage
Performance costmedium
Mobile-safeyes

preloader reveal clip-path splittext customease landing hero minimal stagger

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 — Typographic Preloader with Split-Screen Unveil

Goal

Build a full-screen landing intro that plays automatically once on page load (~7.5s). On a dark preloader, the studio name "Nullspace Studio" drops in character by character through masks while three small grey corner tags flip in; every character except the leading "N" then exits downward, a large "10" drops in beside it, the "N" and the "10" slide toward each other and morph into a logo lockup (the "N" shrinks and goes extra-bold, the "10" blows up to 14rem). Then the dark screen splits in half along a horizontal seam: the top half slides up, the bottom half slides down, and the page content (full-bleed hero image, nav, footer, and a white center card whose title rises per character) is revealed through an expanding clip-path letterbox. One GSAP timeline, one custom hop ease.

Tech

Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) plus the GSAP plugins CustomEase and SplitText. No smooth-scroll library. Register with gsap.registerPlugin(CustomEase, SplitText) and run everything inside DOMContentLoaded.

Layout / HTML

Class names are load-bearing — the JS/CSS query them:

<div class="preloader">
  <div class="intro-title"><h1>Nullspace Studio</h1></div>
  <div class="outro-title"><h1>10</h1></div>
</div>

<div class="split-overlay">
  <div class="intro-title"><h1>Nullspace Studio</h1></div>
  <div class="outro-title"><h1>10</h1></div>
</div>

<div class="tags-overlay">
  <div class="tag tag-1"><p>Negative Space</p></div>
  <div class="tag tag-2"><p>Form & Void</p></div>
  <div class="tag tag-3"><p>Light Studies</p></div>
</div>

<div class="container">
  <nav>
    <p id="logo">N10</p>
    <p>Menu</p>
  </nav>
  <div class="hero-img"><img src="..." alt="" /></div>
  <div class="card"><h1>Nullspace</h1></div>
  <footer>
    <p>Scroll Down</p>
    <p>Made by Nullspace Studio</p>
  </footer>
</div>

Key idea: .split-overlay is an exact duplicate of the preloader's content. It sits underneath the preloader and is pre-positioned (via gsap.set) to the preloader's FINAL logo-lockup state. At the end of the lockup animation the preloader is clipped to the top half of the screen and the split-overlay to the bottom half — a pixel-perfect invisible swap — so the two halves can then slide apart in opposite directions.

Styling

Font (Google Fonts): DM Sans — the only family.

  • * { margin:0; padding:0; box-sizing:border-box }; body { font-family:"DM Sans", sans-serif }.
  • img { width:100%; height:100%; object-fit:cover }.
  • h1 { text-transform:uppercase; font-size:6rem; font-weight:600; line-height:1 }.
  • p { text-transform:uppercase; font-size:13px; font-weight:500 }.

Layers:

  • .preloader, .split-overlay, .tags-overlay { position:fixed; width:100vw; height:100svh }.
  • .preloader, .split-overlay { background-color:#0a0a0a; color:#fff }.
  • z-index: .preloader and .tags-overlay = 2; .split-overlay = 1.
  • .intro-title { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:100%; text-align:center } (applies inside both dark layers).
  • .outro-title { position:absolute; top:50%; left:calc(50% + 10rem); transform:translate(-50%,-50%) }.

Tags:

  • .tag { position:absolute; width:max-content; color:#5a5a5a; overflow:hidden }.
  • .tag-1 { top:15%; left:15% }, .tag-2 { bottom:15%; left:25% }, .tag-3 { bottom:30%; right:15% }.

Page content:

  • .container { position:relative; width:100%; height:100%; min-height:100svh; display:flex; flex-direction:column; justify-content:space-between; z-index:2; clip-path: polygon(0 48%, 0 48%, 0 52%, 0 52%) }initial clip-path is a zero-width sliver at the left edge (all x = 0, y between 48% and 52%), so the content is invisible until the timeline sweeps the slit open.
  • .container .hero-img { position:absolute; width:100%; height:100% } (full-bleed image behind everything in the container).
  • nav, footer { position:relative; width:100vw; padding:2em; display:flex; justify-content:space-between; align-items:center; color:#fff; z-index:2 }. nav p#logo { font-weight:600; font-size:20px }.
  • .card { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:30%; height:70%; display:flex; justify-content:center; align-items:center; background-color:#fff; clip-path: polygon(0% 50%, 100% 50%, 100% 50%, 0% 50%) } — initial clip-path is a degenerate horizontal line at mid-height (card hidden). Its h1 { text-align:center; width:100%; font-size:3rem }.

SplitText piece styles (these create the masks):

  • .intro-title .char, .outro-title .char, .card .char { position:relative; display:inline-block; overflow:hidden } — each char is its own overflow-hidden mask.
  • .intro-title .char, .outro-title .char { margin-top: 0.75rem }.
  • .intro-title .char span, .outro-title .char span, .tag .word { position:relative; display:inline-block; transform:translateY(-100%); will-change:transform } — title chars and tag words start hidden ABOVE their mask.
  • .card .char span { position:relative; display:inline-block; transform:translateY(100%); will-change:transform } — card chars start hidden BELOW their mask.
  • .intro-title .first-char { transform-origin: top left } (matters for its scale tween).

GSAP effect (be exact)

Setup
CustomEase.create("hop", ".8, 0, .3, 1");

SplitText helper — split, then manually wrap each char's text in an inner <span> (the CSS above animates .char span, not .char):

const splitTextElements = (selector, type = "words,chars", addFirstChar = false) => {
  document.querySelectorAll(selector).forEach((element) => {
    const splitText = new SplitText(element, {
      type,
      wordsClass: "word",
      charsClass: "char",
    });
    if (type.includes("chars")) {
      splitText.chars.forEach((char, index) => {
        const originalText = char.textContent;
        char.innerHTML = `<span>${originalText}</span>`;
        if (addFirstChar && index === 0) char.classList.add("first-char");
      });
    }
  });
};

splitTextElements(".intro-title h1", "words, chars", true); // hits BOTH preloader & split-overlay copies
splitTextElements(".outro-title h1");                        // words + chars ("1","0"), both copies
splitTextElements(".tag p", "words");                        // words only, no inner spans
splitTextElements(".card h1", "words, chars", true);

const isMobile = window.innerWidth <= 1000;
Pre-position the split-overlay duplicate to the FINAL state
gsap.set(
  [".split-overlay .intro-title .first-char span",
   ".split-overlay .outro-title .char span"],
  { y: "0%" }                          // its "N" and "10" are already visible
);
gsap.set(".split-overlay .intro-title .first-char", {
  x: isMobile ? "7.5rem" : "18rem",
  y: isMobile ? "-1rem" : "-2.75rem",
  fontWeight: "900",
  scale: 0.75,
});
gsap.set(".split-overlay .outro-title .char", {
  x: isMobile ? "-3rem" : "-8rem",
  fontSize: isMobile ? "6rem" : "14rem",
  fontWeight: "500",
});

(The non-first intro chars of the split-overlay keep their CSS translateY(-100%), i.e. hidden — the duplicate shows only the finished "N + 10" lockup.)

The timeline

One timeline, gsap.timeline({ defaults: { ease: "hop" } }). All position params below are absolute times in seconds. tags = gsap.utils.toArray(".tag").

  1. Tags in — for each tag i (0,1,2), at time 0.5 + i * 0.1: its p .word{ y: "0%", duration: 0.75 } (words drop down into view from -100%).
  2. Intro title in — at 0.5: ".preloader .intro-title .char span"{ y: "0%", duration: 0.75, stagger: 0.05 }. "Nullspace Studio" drops in char by char from above through the per-char masks.
  3. Intro title out (except first char) — at 2: ".preloader .intro-title .char:not(.first-char) span"{ y: "100%", duration: 0.75, stagger: 0.05 }. Every char except the leading "N" continues DOWN and out of its mask (in through the top, out through the bottom).
  4. Outro "10" in — at 2.5: ".preloader .outro-title .char span"{ y: "0%", duration: 0.75, stagger: 0.075 }.
  5. Slide together — at 3.5, two simultaneous 1s tweens:
  6. ".preloader .intro-title .first-char"{ x: isMobile ? "9rem" : "21.25rem", duration: 1 } (the "N" travels right toward center; it overshoots its final x slightly).
  7. ".preloader .outro-title .char"{ x: isMobile ? "-3rem" : "-8rem", duration: 1 } (the "10" chars travel left).
  8. Morph into logo lockup — at 4.5, two simultaneous 0.75s tweens:
  9. ".preloader .intro-title .first-char"{ x: isMobile ? "7.5rem" : "18rem", y: isMobile ? "-1rem" : "-2.75rem", fontWeight: "900", scale: 0.75, duration: 0.75 } — the "N" pulls back/up, shrinks to 75% (transform-origin top left) and becomes extra-bold.
  10. ".preloader .outro-title .char"{ x: isMobile ? "-3rem" : "-8rem", fontSize: isMobile ? "6rem" : "14rem", fontWeight: "500", duration: 0.75 } — the "10" scales up via animated fontSize (6rem → 14rem on desktop). These values EXACTLY match the split-overlay gsap.set pre-positioning.
  11. The second tween's onComplete performs the invisible swap:

``js gsap.set(".preloader", { clipPath: "polygon(0 0, 100% 0, 100% 50%, 0 50%)" }); // top half gsap.set(".split-overlay", { clipPath: "polygon(0 50%, 100% 50%, 100% 100%, 0 100%)" }); // bottom half `` Since both layers now render identical pixels, nothing appears to change.

  1. Letterbox slit opens — at 5: ".container"{ clipPath: "polygon(0% 48%, 100% 48%, 100% 52%, 0% 52%)", duration: 1 }. The content's clip sweeps from the zero-width left-edge sliver to a thin full-width band between 48% and 52% viewport height — a glowing slit of the hero image appears across the seam.
  2. Tags out — for each tag i, at 5.5 + i * 0.1: its p .word{ y: "100%", duration: 0.75 } (exit downward).
  3. The split — at 6, three overlapping tweens:
  4. [".preloader", ".split-overlay"]{ y: (i) => (i === 0 ? "-50%" : "50%"), duration: 1 } — top half slides up offscreen, bottom half slides down.
  5. ".container"{ clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)", duration: 1 } — the letterbox expands to full screen in sync with the halves parting.
  6. at 6.25: ".container .card"{ clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)", duration: 0.75 } — the white card unmasks vertically from its center line.
  7. Card title in — at 6.5: ".container .card h1 .char span"{ y: "0%", duration: 0.75, stagger: 0.05 } — "Nullspace" rises from BELOW (its spans start at translateY(100%), unlike the title chars which started at -100%).
Ease reference
  • hop = CustomEase.create("hop", ".8, 0, .3, 1") — used by every tween (timeline default). No other eases.

Assets / images

One image: a full-bleed hero background revealed behind the split — a wide, desaturated architectural interior with strong negative space and directional light (calm, minimal, editorial mood). Landscape orientation (roughly 16:9), displayed with object-fit: cover filling the viewport. White nav/footer text sits on top of it, so darker/muted imagery reads best.

Behavior notes

  • Autoplay once on load; no scroll/hover/click triggers, no ScrollTrigger. Total runtime ≈ 7.25s.
  • isMobile breakpoint at window.innerWidth <= 1000 swaps the rem offsets/font sizes as listed above.
  • Responsive CSS (max-width: 1000px): h1 { font-size:2.5rem }; .outro-title { left: calc(50% + 4rem) }; .card { width:75% }; .card h1 { font-size:2.5rem }; .intro-title .char, .outro-title .char { margin-top:0.5rem }.
  • Use 100svh for all full-height layers so mobile browser chrome doesn't clip.
  • The preloader/split-overlay never get removed from the DOM — after sliding ±50% they sit offscreen behind the revealed page (the container is z-index 2, above the split-overlay's z-index 1).
  • Keep the will-change: transform hints on the animated .char span / .word elements.