All components

FameEstate Scroll Animation

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

Open live demo ↗ Raw prompt (.md)

What it does

Pinned hero scrubbed over seven viewport heights with Lenis smooth scroll: a red panel opens from a thin clip-path slit to full screen, three images cascade in with staggered clip-path and scale reveals, then an outro panel scales in and splits into two clip-path halves that slide apart to uncover the about section.

How it's built

Categoryscroll
Techgsap, lenis
GSAP pluginsScrollTrigger
Complexitypage
Performance costlight
Mobile-safeyes
Scrollhijacks scrolling

scroll pin scrub clip-path reveal lenis hero 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

FameEstate Scroll Animation — Pinned Clip-Path Hero Reveal

Goal

Build a full-screen luxury real-estate hero that is pinned and scrubbed over seven viewport heights while a single GSAP timeline plays a cinematic reveal, then hands off to a plain about section. In order, as you scroll: the full-bleed background image zooms out (scale 1.5 → 1); a mustard-gold panel (the "revealer") opens from a hair-thin vertical seam at screen center — first growing top-to-bottom, then wiping outward left-and-right to fill the screen; three full-bleed images cascade in one after another from a collapsed center point, each expanding via clip-path while scaling 0 → 1; a gold outro panel with a heading scales in, then splits down the middle into two halves that slide apart (left half off-screen left, right half off-screen right) to uncover the about section beneath. Smooth scroll via Lenis. The whole hero is one scrubbed ScrollTrigger timeline.

Tech

Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) plus the GSAP plugin ScrollTrigger, and lenis (npm) for smooth scroll. No other plugins, no framework — plain Vite-style module imports:

import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import Lenis from "lenis";

Register once: gsap.registerPlugin(ScrollTrigger);. Run everything inside document.addEventListener("DOMContentLoaded", …).

Layout / HTML

Two top-level <section>s. Class names are load-bearing — the JS/CSS query them.

<section class="hero">
  <div class="hero-bg"><img src="…" alt="" /></div>

  <div class="hero-content">
    <h1>A modern approach to luxury living and timeless spaces</h1>
  </div>

  <div class="hero-revealer"></div>

  <div class="hero-images">
    <div class="hero-img"><img src="…" alt="" /></div>
    <div class="hero-img"><img src="…" alt="" /></div>
    <div class="hero-img"><img src="…" alt="" /></div>
  </div>

  <div class="hero-outro-content">
    <h1>Thoughtfully crafted spaces designed to inspire modern living connections</h1>
  </div>
</section>

<section class="about">
  <div class="about-content">
    <h3>Designing digital experiences that feel effortless</h3>
    <p>From initial concept to final detail, every element is considered with purpose and clarity</p>
  </div>
</section>

Notes:

  • .hero-images holds exactly 3 .hero-img wrappers (each with one <img>).
  • .hero-outro-content is a single element in the markup — the JS clones it at runtime and turns the pair into left/right halves (see GSAP section). Both the original and the clone keep the hero-outro-content class.
  • Use the neutral editorial copy above verbatim (uppercase real-estate/design prose). "FameEstate" is the fictional demo brand — no real client names.

Styling

Font (Google Fonts): DM Sans — import the full optical-size/weight axis:

@import url("https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&display=swap");

Palette (CSS variables):

  • --base-100: #f0f0e6 — page background (warm off-white/cream); also the text color on the hero panels.
  • --base-200: #997f1f — mustard/olive-gold; the color of BOTH the hero-revealer and the hero-outro-content panel.
  • --base-300: #0f0f0f — near-black; the about section text color.

Global / reset:

  • * { margin:0; padding:0; box-sizing:border-box; }
  • body { font-family:"DM Sans", sans-serif; background-color:var(--base-100); }
  • img { width:100%; height:100%; object-fit:cover; }
  • h1, h3, p { text-transform:uppercase; font-weight:500; line-height:1; }
  • h1 { font-size: clamp(3rem, 4vw, 5rem); }
  • h3 { font-size: clamp(2rem, 3vw, 4rem); }
  • p { font-size: 1.1rem; }

.hero:

  • position:relative; width:100%; height:100svh; overflow:hidden; z-index:2; (sits ABOVE the about section, which is z-index:1).

Full-cover overlays — .hero-bg, .hero-content, .hero-revealer, .hero-images all share:

  • position:absolute; top:0; left:0; width:100%; height:100%; will-change:transform;

Centered/scaled layers — .hero-img, .hero-outro-content share:

  • position:absolute; top:50%; left:50%; width:100%; height:100%;
  • transform: translate(-50%, -50%) scale(0);both start scaled to zero (this is the initial state GSAP scales up from).
  • will-change:transform;

Text panels — .hero-content, .hero-outro-content:

  • padding:2rem; display:flex; justify-content:center; align-items:center; text-align:center; color:var(--base-100);
  • Their h1 is constrained: .hero-content h1, .hero-outro-content h1 { width:65%; }.

Panel-specific:

  • .hero-outro-content { background-color: var(--base-200); } — the gold outro panel.
  • .hero-bg { transform: scale(1.5); }background starts zoomed in 1.5× (GSAP zooms it out to 1).
  • .hero-revealer { background-color: var(--base-200); clip-path: polygon(49.5% 50%, 50.5% 50%, 50.5% 50%, 49.5% 50%); }initial clip is a 1%-wide, zero-height sliver collapsed at the exact center (all four points at y:50%). Effectively invisible until animated. will-change:transform (from the shared rule).
  • .hero-img { clip-path: polygon(50% 50%, 50% 50%, 50% 50%, 50% 50%); will-change:clip-path; }initial clip is a single point at the center (all four polygon points at 50% 50%), fully collapsed.

.about:

  • position:relative; width:100%; height:100svh; padding:2rem; background-color:var(--base-100); color:var(--base-300); z-index:1;
  • margin-top: 500svh; — CRITICAL. This 5-viewport gap is the scroll runway the pinned hero consumes (see behavior notes). Without it there is no room to scroll through the pinned timeline.
  • .about-content { width:40%; height:100%; margin:0 auto; display:flex; flex-direction:column; justify-content:space-between; text-align:center; } (heading at top, paragraph at bottom).

GSAP effect (the important part — be exact)

Smooth scroll wiring (Lenis + GSAP ticker)
const lenis = new Lenis();
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => { lenis.raf(time * 1000); });
gsap.ticker.lagSmoothing(0);

Lenis' scroll events call ScrollTrigger.update; Lenis is driven by GSAP's ticker; lag smoothing is off so scrub stays glued to the scroll position.

DOM prep — clone the outro into two halves

Grab the elements, then clone the outro panel so there are two identical stacked copies, one clipped to each half:

const heroSection       = document.querySelector(".hero");
const heroBackground    = document.querySelector(".hero-bg");
const heroContent       = document.querySelector(".hero-content");
const heroRevealer      = document.querySelector(".hero-revealer");
const heroImagesWrapper = document.querySelector(".hero-images");
const heroImages        = gsap.utils.toArray(".hero-img");        // length 3
const heroOutroContent  = document.querySelector(".hero-outro-content");

const heroOutroClone = heroOutroContent.cloneNode(true);
heroOutroContent.classList.add("hero-outro-left");   // original → left half
heroOutroClone.classList.add("hero-outro-right");    // clone → right half
heroOutroContent.parentNode.appendChild(heroOutroClone);

gsap.set(".hero-outro-left",  { clipPath: "polygon(0% 0%, 50% 0%, 50% 100%, 0% 100%)" });   // shows LEFT half
gsap.set(".hero-outro-right", { clipPath: "polygon(50% 0%, 100% 0%, 100% 100%, 50% 100%)" }); // shows RIGHT half
gsap.set(heroImagesWrapper, { scale: 1 });

Both halves still carry the hero-outro-content class, so a tween targeting .hero-outro-content animates BOTH at once. Each is a full-size gold panel clipped to opposite halves; overlapped, they read as one panel with the heading centered.

The scrubbed timeline (one ScrollTrigger)
const heroScrollTimeline = gsap.timeline({
  scrollTrigger: {
    trigger: heroSection,
    start: "top top",
    end: () => `+=${window.innerHeight * 7}`,   // scrub across 7 viewport heights
    pin: true,
    pinSpacing: false,          // no spacer — the about section's 500svh margin is the runway
    scrub: true,
    invalidateOnRefresh: true,
  },
});

This is a scrub timeline built with explicit position parameters — every tween below is placed at an absolute time on a 0 → 1.0 timeline (the numbers are relative "seconds" that get linearly mapped to the 7vh scroll distance). No ease is specified on any tween, so GSAP's default power1.out shapes each tween over its own segment. Add the tweens in this exact order, with these exact positions and durations:

  1. Background zoom-out — pos 0, dur 0.5:

heroBackground → { scale: 1 } (from CSS scale(1.5)1). The landscape slowly settles/zooms out over the first half of the timeline.

  1. Revealer seam grows vertically — pos 0, dur 0.2:

heroRevealer → { clipPath: "polygon(49.5% 0%, 50.5% 0%, 50.5% 100%, 49.5% 100%)" }. From the collapsed center sliver to a 1%-wide full-height vertical bar at screen center — a thin gold seam opening top-to-bottom.

  1. Revealer wipes open horizontally — pos 0.2, dur 0.3:

heroRevealer → { clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)" }. The thin vertical bar expands outward to a full rectangle — the gold panel wipes the screen from center to both edges.

  1. Three images cascade in — a forEach over the 3 heroImages, each placed at cascadeStart + index * cascadeStagger, where:

``js const cascadeStart = 0.4; // first image at 0.40 const cascadeStagger = 0.04; // then 0.44, then 0.48 const cascadeDuration = 0.16; ` Each tween: heroImage → { clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)", scale: 1, duration: 0.16 }. From the CSS start (clip-path collapsed to the center point + scale(0)) to a full-rectangle clip at scale(1)`. Result: each image irises open from a point at center while scaling up, one after the other (positions 0.40, 0.44, 0.48), each new image landing on top of the previous.

  1. Outro panel scales in — pos cascadeStart + heroImages.length * cascadeStagger + cascadeStagger * 0.5 = 0.4 + 0.12 + 0.02 = 0.54, dur 0.16:

".hero-outro-content" → { scale: 1 } (both halves, from CSS scale(0)1). The gold heading panel pops in over the last cascaded image.

  1. Hard hide the earlier layers — pos 0.7 (a .set, instantaneous):

heroScrollTimeline.set([heroBackground, heroContent, heroRevealer, heroImagesWrapper], { autoAlpha: 0 }). Background, hero heading, revealer, and the images wrapper all snap invisible so only the gold outro remains.

  1. Section background → transparent — pos 0.7 (a .set):

heroScrollTimeline.set(heroSection, { backgroundColor: "transparent" }). So that when the outro halves part, the about section shows through instead of the hero's own background.

  1. Left half slides off left — pos 0.7, dur 0.3:

".hero-outro-left" → { xPercent: -150 }. The left half rockets fully off-screen to the left.

  1. Right half slides off right — pos 0.7, dur 0.3:

".hero-outro-right" → { xPercent: 50 }. The right half slides right by half its width — enough to push its visible (right-half) content off the right edge.

Steps 8 & 9 run simultaneously (both at pos 0.7), splitting the gold panel down the seam and drawing the two halves apart to unveil the about section. The timeline's last tweens end at pos 1.0 (total timeline length = 1.0), exactly as the scrub reaches the end of the 7vh runway.

No SplitText, no CustomEase, no lerp/rAF interpolation, no Three.js. The only motion is this single scrubbed, pinned timeline of clip-path / scale / xPercent tweens, all on GSAP's default power1.out ease.

Assets / images

Four full-bleed landscape photos (all roughly 16:9, ~1456×816), each rendered at 100% width/height with object-fit: cover so they fill the entire viewport (exact crop doesn't matter — cover handles it). Editorial, cinematic set. Roles:

  • hero-bg — the establishing background that starts zoomed 1.5× and settles to 1. A golden-hour landscape of misty rolling hills with scattered pine trees, low fog drifting through the valleys, hazy backlit sun. Dominant colors: warm gold, amber, soft cream sky, muted green.
  • hero-img #1 — a dramatic backlit portrait: the profile silhouette of a person against crossing red spotlight beams forming an X on a dark stage. Dominant colors: deep black silhouette, intense red and orange glow.
  • hero-img #2 — a product still-life: a plain glossy white cosmetic bottle with a black cap resting on a smooth stone, framed by dried beige gypsophila/baby's-breath flowers against a neutral taupe backdrop. Dominant colors: soft beige, tan, warm neutral, black accent.
  • hero-img #3 — a coastal landscape: rugged tan sea cliffs dropping into deep blue ocean at soft evening light, distant headland and a pale cloudy sky. Dominant colors: warm sandstone tan, deep teal-blue water, muted sky.

Describe generically by role and form — no brands. Each hero-img irises open from a center point while scaling up, one after another. If you have fewer than four, repeat.

Behavior notes

  • Desktop-first. At max-width: 1000px, the constrained widths relax to full width: .hero-content h1, .hero-outro-content h1, .about-content { width: 100%; }. The pin + scrub effect runs at all sizes.
  • Heights use svh (100svh, margin-top: 500svh) so mobile browser chrome doesn't break the full-screen layout.
  • The about section's margin-top: 500svh is the scroll runway — because the pin uses pinSpacing: false, no spacer is inserted, so the document's own height (hero 1vh + 5vh margin + about 1vh ≈ the +=7*innerHeight end) must supply the scroll distance. Keep them in sync.
  • invalidateOnRefresh: true + the function-based end recompute the 7vh distance on resize.
  • Nothing autoplays; the entire sequence is scroll-scrubbed (scrub: true), so it plays forward as you scroll down and reverses as you scroll up. No reduced-motion handling in the original.
  • Keep the will-change hints (transform on the overlay layers, clip-path on .hero-img) — they matter for smooth clip-path animation.