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-imagesholds exactly 3.hero-imgwrappers (each with one<img>)..hero-outro-contentis 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 thehero-outro-contentclass.- 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 thehero-revealerand thehero-outro-contentpanel.--base-300: #0f0f0f— near-black; theaboutsection 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 isz-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
h1is 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 aty: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 at50% 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:
- Background zoom-out — pos
0, dur0.5:
heroBackground → { scale: 1 } (from CSS scale(1.5) → 1). The landscape slowly settles/zooms out over the first half of the timeline.
- Revealer seam grows vertically — pos
0, dur0.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.
- Revealer wipes open horizontally — pos
0.2, dur0.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.
- Three images cascade in — a
forEachover the 3heroImages, each placed atcascadeStart + 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.
- Outro panel scales in — pos
cascadeStart + heroImages.length * cascadeStagger + cascadeStagger * 0.5=0.4 + 0.12 + 0.02 = 0.54, dur0.16:
".hero-outro-content" → { scale: 1 } (both halves, from CSS scale(0) → 1). The gold heading panel pops in over the last cascaded image.
- 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.
- 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.
- Left half slides off left — pos
0.7, dur0.3:
".hero-outro-left" → { xPercent: -150 }. The left half rockets fully off-screen to the left.
- Right half slides off right — pos
0.7, dur0.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
aboutsection'smargin-top: 500svhis the scroll runway — because the pin usespinSpacing: false, no spacer is inserted, so the document's own height (hero 1vh + 5vh margin + about 1vh ≈ the+=7*innerHeightend) must supply the scroll distance. Keep them in sync. invalidateOnRefresh: true+ the function-basedendrecompute 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-changehints (transformon the overlay layers,clip-pathon.hero-img) — they matter for smooth clip-path animation.