All components

Kin 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 two-panel white revealer wipes open with a custom 'hop' ease, a stack of full-bleed images scales in from 1.5x and fades sequentially, then GSAP Flip morphs the final three images into a small stacked cluster in the bottom-left corner while masked hero text (logo, nav, address, SplitType heading lines) slides up and a team image clip-path reveals. Fully automatic, plays once on DOMContentLoaded.

How it's built

Categorypreloader
Techgsap, split-type
GSAP pluginsFlip, CustomEase
Complexitypage
Performance costmedium
Mobile-safeyes

reveal flip clip-path split-type landing intro hero stagger 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

Landing Page Reveal — Revealer Wipe → Image Stack → Flip-to-Corner Intro

Goal

Build a full-screen editorial fashion landing hero with a cinematic load-triggered intro (~8.6 s, plays once). Two white panels (top + bottom) split apart with a custom hop ease to reveal a full-bleed image; a deck of 8 images scales down from 1.5× and fades in one after another so they cascade into place; then GSAP Flip shrinks the final three "main" images from full-screen down to a small stacked cluster of thumbnails in the bottom-left corner. Simultaneously the hero furniture slides in — the logo, nav links, address and a SplitType-split heading all rise up from behind clip-path masks, and a desaturated team image in the bottom-right unmasks upward. Everything is orchestrated by nested GSAP timelines that fire automatically on DOMContentLoaded.

Tech

Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) plus the GSAP plugins Flip and CustomEase, and split-type (npm, imported as SplitType) for the line-splitting. No smooth-scroll library — the page does not scroll during the intro; it is a pure load-triggered timeline. Register with gsap.registerPlugin(Flip, CustomEase) and fire the whole sequence on DOMContentLoaded.

Imports:

import gsap from "gsap";
import { Flip } from "gsap/Flip";
import { CustomEase } from "gsap/CustomEase";
import SplitType from "split-type";
gsap.registerPlugin(Flip, CustomEase);

Layout / HTML

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

<div class="container">
  <div class="revealers">
    <div class="revealer r-1"></div>   <!-- top white panel -->
    <div class="revealer r-2"></div>   <!-- bottom white panel -->
  </div>

  <div class="images">
    <div class="img"><img src="..." alt="" /></div>          <!-- 1: first shown -->
    <div class="img"><img src="..." alt="" /></div>          <!-- 2 -->
    <div class="img"><img src="..." alt="" /></div>          <!-- 3 -->
    <div class="img"><img src="..." alt="" /></div>          <!-- 4 -->
    <div class="img"><img src="..." alt="" /></div>          <!-- 5 -->
    <div class="img main"><img src="..." alt="" /></div>     <!-- 6: Flip target -->
    <div class="img main"><img src="..." alt="" /></div>     <!-- 7: Flip target -->
    <div class="img main"><img src="..." alt="" /></div>     <!-- 8: Flip target -->
  </div>

  <div class="hero-content">
    <div class="site-logo">
      <div class="word"><h1>Arc</h1></div>
      <div class="word"><h1>Worldwide<sup>&copy;</sup></h1></div>
    </div>

    <div class="nav">
      <div class="nav-item"><p>About</p></div>
      <div class="nav-item"><p>Work</p></div>
      <div class="nav-item"><p>Journal</p></div>
      <div class="nav-item"><p>Contact</p></div>
    </div>

    <div class="team-img"><img src="..." alt="" /></div>   <!-- reuse image 3 -->

    <div class="site-info">
      <div class="row">
        <div class="col"><div class="line"><p>Featured Works</p></div></div>
        <div class="col">
          <h2>Arc is a contemporary fashion brand redefining elegance with timeless designs and innovative aesthetics.</h2>
        </div>
      </div>
      <div class="row">
        <div class="col"></div>
        <div class="col">
          <div class="address">
            <div class="line"><p>Arc Studio</p></div>
            <div class="line"><p>Riverstone Building</p></div>
            <div class="line"><p>- 28 Orchard Lane</p></div>
            <div class="line"><p>N1 4DX</p></div>
          </div>
          <div class="socials">
            <div class="line"><p>SayHi@Arc.com</p></div>
            <br />
            <div class="line"><p>Instagram</p></div>
            <div class="line"><p>LinkedIn</p></div>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Notes:

  • .images holds exactly 8 .img wrappers. The last three (indices 5,6,7) also carry class main — those are the Flip targets that survive into the corner stack; the first five are .remove()d from the DOM mid-sequence.
  • .team-img reuses the same photo as image 3 (bottom-right, rendered grayscale).
  • Use "Arc" / "Worldwide" as the neutral placeholder brand. Copyright superscript © after "Worldwide".

Styling

Fonts: the design calls for "PP Neue Montreal" (body + all h1/h2) and "Apercu Mono Pro" (the small .site-info p labels). These are custom faces — if unavailable, fall back to a clean geometric sans (e.g. Helvetica Neue / Inter) for the display type and a monospace for the small labels. Keep the family names in the CSS with sensible fallbacks.

Palette: dead simple — background #fff, all text #000. The revealer panels are also #fff.

Global:

  • * { margin:0; padding:0; box-sizing:border-box }.
  • html, body { width:100%; height:100%; font-family:"PP Neue Montreal"; background:#fff }.
  • img { width:100%; height:100%; object-fit:cover }.

Containers:

  • .container: position:relative; width:100vw; height:100vh; overflow:hidden.
  • .revealers: position:fixed; inset:0; width:100vw; height:100vh; display:flex; flex-direction:column; z-index:2. Each .revealer: flex:1; width:100%; background:#fff (so the two panels each fill 50% of the viewport height). Initial clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%) (full rectangle — both panels fully opaque and covering everything).
  • .images: position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:100%; height:100%; transform-origin:center center; will-change:transform.
  • .img: position:absolute; top:50%; left:50%; transform:translate(-50%,-50%) scale(1.5); width:100%; height:100%; opacity:0; will-change:transform. .img:first-child { opacity:1 } (only the first image is visible at the start; all start zoomed to 1.5×).

Stacked (post-Flip) state — these classes are added by the JS, but the CSS must define them:

  • .images.stacked-container: position:fixed; left:2em; bottom:2em; width:auto; height:auto; display:flex; flex-direction:column-reverse; align-items:flex-start; gap:1em; transform:none; will-change:transform.
  • .img.stacked: position:relative; width:150px; height:100px; transform:none; top:auto; left:auto; opacity:1; will-change:transform.

Hero furniture:

  • .hero-content: position:relative; width:100%; height:100%.
  • .site-logo: position:absolute; top:2em; left:2em; display:flex; gap:1em. .site-logo h1: color:#000; font-size:5vw; font-weight:500; line-height:1; letter-spacing:-0.01em. .site-logo h1 sup: position:absolute; top:-0.125em; font-size:2rem.
  • .nav: position:absolute; right:0; width:50%; padding:2em; display:flex; justify-content:flex-end; gap:1em. .nav .nav-item p: font-size:16px; font-weight:500.
  • .team-img: position:absolute; right:2em; bottom:2em; width:40%; height:50%. Initial clip-path: polygon(0 100%, 100% 100%, 100% 100%, 0% 100%) (collapsed onto the bottom edge → hidden). .team-img img { filter:saturate(0) } (grayscale).
  • .site-info: position:absolute; bottom:2em; left:2em; width:50%; height:50%; display:flex; flex-direction:column; justify-content:space-between. .site-info .row: display:flex; gap:2em; .row .col { flex:1 }. .site-info h2: font-size:25px; font-weight:500; line-height:1.25. .site-info p: text-transform:uppercase; font-family:"Apercu Mono Pro"; font-size:11px; font-weight:500; color:#000; line-height:1.25. The second row's second col is display:flex so .address and .socials sit side by side (each flex:1).

Mask / initial hidden states (critical — the reveal depends on these):

  • .word, .nav-item, .line { clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%) } — each is an overflow-clipping mask window.
  • .word h1, .nav-item p, .line p { position:relative; will-change:transform; transform:translateY(100%) } — the text sits one full line below its mask (hidden).
  • .site-info h2 .line span { display:block; transform:translateY(100%) } — the split heading lines also start pushed one line down inside their masks.

GSAP effect (be exact)

Setup
document.addEventListener("DOMContentLoaded", () => {
  CustomEase.create("hop",  "M0,0 C0.355,0.022 0.448,0.079 0.5,0.5 0.542,0.846 0.615,1 1,1");
  CustomEase.create("hop2", "M0,0 C0.078,0.617 0.114,0.716 0.255,0.828 0.373,0.922 0.561,1 1,1");
  • hop — a slightly "sticky" ease-in-out (hangs near the middle, then snaps to the end). Used for the revealer wipe, the Flip, and the team-image unmask.
  • hop2 — an ease-out-ish curve that leaps early then settles. Used for the masked-text rise.

SplitType on the heading, then re-wrap each line in a mask. Split .site-info h2 into lines, then for every produced line replace it with a <div class="line"><span>…</span></div> wrapper (so it picks up the .line clip-path mask and the span { translateY(100%) } init):

const splitH2 = new SplitType(".site-info h2", { types: "lines" });
splitH2.lines.forEach((line) => {
  const text = line.textContent;
  const wrapper = document.createElement("div");
  wrapper.className = "line";
  const span = document.createElement("span");
  span.textContent = text;
  wrapper.appendChild(span);
  line.parentNode.replaceChild(wrapper, line);
});

Three timelines:

const mainTl     = gsap.timeline();
const revealerTl = gsap.timeline();
const scaleTl    = gsap.timeline();
revealerTl — the two white panels split apart (both start together, "<")
revealerTl
  .to(".r-1", {
    clipPath: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)",   // bottom edge collapses UP to the top → wipes up
    duration: 1.5, ease: "hop",
  })
  .to(".r-2", {
    clipPath: "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)", // top edge collapses DOWN to the bottom → wipes down
    duration: 1.5, ease: "hop",
  }, "<");   // simultaneous

The top panel wipes upward and the bottom panel wipes downward at the same time, splitting open like a horizontal shutter over 1.5 s to reveal the image deck behind.

scaleTl — image deck scales in and cascades
scaleTl.to(".img:first-child", { scale: 1, duration: 2, ease: "power4.inOut" });

const images = document.querySelectorAll(".img:not(:first-child)"); // the other 7
images.forEach((img) => {
  scaleTl.to(img, {
    opacity: 1, scale: 1, duration: 1.25, ease: "power3.out",
  }, ">-0.95");   // start 0.95s BEFORE the previous tween ends → heavy overlap
});
  • The already-visible first image de-zooms scale 1.5 → 1 over 2 s (power4.inOut).
  • Each of the other 7 images fades opacity 0 → 1 and de-zooms 1.5 → 1 over 1.25 s (power3.out), each inserted at ">-0.95" (0.95 s before the previous one finishes). Because duration 1.25 − 0.95 = ~0.3 s effective step, the images cascade in quick succession, each layer landing on top of the last. scaleTl total ≈ 4.1 s.
mainTl — the master sequence
mainTl
  .add(revealerTl)              // t = 0 .. 1.5
  .add(scaleTl, "-=1.25")       // inserted at 0.25 → images start scaling while panels still opening
  .add(() => { /* Flip, see below */ })   // fires at ~t = 4.35 (end of scaleTl)
  .to(".word h1, .nav-item p, .line p, .site-info h2 .line span", {
    y: 0, duration: 3, ease: "hop2", stagger: 0.1, delay: 1.25,
  })
  .to(".team-img", {
    clipPath: "polygon(0% 100%, 100% 100%, 100% 0%, 0% 0%)",
    duration: 2, ease: "hop", delay: -4.75,
  });

Placement math (position params / delays resolve to absolute times):

  • revealerTl at 0 → spans [0, 1.5].
  • scaleTl at "-=1.25" → inserted at 0.25 → spans [0.25, ~4.35]. So the deck begins de-zooming while the revealer shutter is still opening.
  • The Flip callback is appended at the end (~4.35 s) — a zero-duration .add(fn).
  • The masked-text .to(...) is appended at ~4.35 with delay: 1.25 → animates [5.6, 8.6].
  • The team-image .to(...) is appended at the new end (~8.6) with delay: -4.75 → pulled back to [3.85, 5.85] (overlaps the Flip and the text rise).

Total runtime ≈ 8.6 s.

The Flip callback (the signature move) — full-screen images → corner thumbnail stack

Fires at ~t = 4.35. It captures the current geometry of the three .main images (currently full-bleed and centered), reparents/restyles them into a small bottom-left column, then Flips from the old geometry to the new:

() => {
  // 1. Drop the first five images out of the DOM — only the 3 .main survive.
  document.querySelectorAll(".img:not(.main)").forEach((img) => img.remove());

  // 2. Snapshot the .main images while they're still full-screen.
  const state = Flip.getState(".main");

  // 3. Restyle the container + images into the stacked-corner layout.
  const imagesContainer = document.querySelector(".images");
  imagesContainer.classList.add("stacked-container");        // → fixed bottom-left, column-reverse, gap 1em
  document.querySelectorAll(".main").forEach((img, i) => {
    img.classList.add("stacked");                            // → position:relative, 150×100px
    img.style.order = i;
    gsap.set(".img.stacked", { clearProps: "transform,top,left" });
  });

  // 4. Animate from the snapshot to the new layout.
  return Flip.from(state, {
    duration: 2,
    ease: "hop",
    absolute: true,
    stagger: { amount: -0.3 },   // reverse-order stagger, 0.3s total spread
  });
}

Visual result: the three overlapping full-screen images simultaneously shrink and fly down to the bottom-left corner, landing as a tidy vertical stack of three 150×100 thumbnails (gap 1em, column-reverse order), the reverse stagger (amount: -0.3) making the last one lead. absolute: true lets them animate freely from their overlapping full-bleed positions. Runs 2 s, independent of mainTl (the returned tween is standalone).

Masked text rise (hop2, t = 5.6 → 8.6)
.to(".word h1, .nav-item p, .line p, .site-info h2 .line span", {
  y: 0, duration: 3, ease: "hop2", stagger: 0.1, delay: 1.25,
})

Every masked text piece — the two logo words (Arc, Worldwide©), the four nav links, the "Featured Works" / address / socials lines, and each split heading line — slides from translateY(100%) (parked below, clipped by its .word / .nav-item / .line mask) up to y:0. Duration 3 s, ease hop2, staggered 0.1 s across all pieces in DOM order.

Team image unmask (hop, t = 3.85 → 5.85)
.to(".team-img", {
  clipPath: "polygon(0% 100%, 100% 100%, 100% 0%, 0% 0%)",
  duration: 2, ease: "hop", delay: -4.75,
})

The bottom-right grayscale image reveals from its collapsed bottom edge upward — its two top corners travel from y:100% to y:0%, so the frame grows up from the bottom to fill its box. Duration 2 s, ease hop.

Timeline summary (absolute seconds)

| t (s) | what | |-----------|------| | 0 – 1.5 | revealer panels split apart (r-1 up, r-2 down), hop | | 0.25 – 2.25 | first image de-zooms 1.5→1, power4.inOut | | ~1.3 – 4.35 | other 7 images fade in + de-zoom, ~0.3s cascade, power3.out | | ~4.35 – 6.35 | Flip: 3 main images shrink/fly to bottom-left thumbnail stack, hop, reverse stagger | | 3.85 – 5.85 | team image clip-path unmasks upward, hop | | 5.6 – 8.6 | logo/nav/address/heading lines rise up from masks, hop2, stagger 0.1 |

Ease reference
  • hop = CustomEase.create("hop", "M0,0 C0.355,0.022 0.448,0.079 0.5,0.5 0.542,0.846 0.615,1 1,1") — revealer wipe, Flip, team-image unmask.
  • hop2 = CustomEase.create("hop2", "M0,0 C0.078,0.617 0.114,0.716 0.255,0.828 0.373,0.922 0.561,1 1,1") — masked text rise.
  • Image deck: first image power4.inOut; the other seven power3.out.

Assets / images

8 editorial fashion photographs, all rendered full-bleed (object-fit: cover), so exact aspect ratios are flexible since they are cover-cropped. Roles:

  • Images 1–5 — the hero deck layers that scale in and fade; they are .remove()d once the Flip fires, so they're only seen briefly stacking up. Image 2 reads best in portrait orientation. Image 3 is also reused (grayscale/desaturated) as the bottom-right .team-img.
  • Images 6–8 — the three .main frames that survive the Flip and become the 150×100 (≈3:2 landscape) corner thumbnails. Image 7 works well roughly square.

All are moody, high-fashion editorial portraits (single subject, studio or atmospheric backdrop) with a cohesive elegant, minimal look. No brand marks. If you have fewer than eight, repeat images to fill the deck; the three used in the corner stack (6–8) should be distinct so the final cluster reads as three photos.

Behavior notes

  • Autoplay once on DOMContentLoaded; no scroll, hover, or click triggers. The page does not scroll during the intro (.container is overflow:hidden, viewport-sized).
  • The first five .img wrappers are permanently removed from the DOM once the Flip fires — do not rely on them existing afterward.
  • Keep every will-change hint (transform on .images/.img/.stacked, and on the masked text pieces) — they matter for smooth transform + clip-path animation.
  • The whole piece is viewport-height (100vh) full-screen; it is a preloader-style hero, so it is intended to sit above the fold and animate on first paint.