Landing Page Reveal — Preloader-to-Hero Intro
Goal
Build a full-screen fashion/editorial landing hero with a cinematic preloader-to-hero reveal that plays automatically once on page load (~7 seconds). A dark overlay counts 0 → 100 while a small masked label steps through three words; five portrait images rise from below, collapse their gap and scale up; the four side images wipe away upward with a clip-path while the center image scales to fill the screen; finally the dark overlay wipes upward to reveal a giant hero name that slides in word by word. Everything is driven by three parallel GSAP timelines sharing one custom ease called hop.
Tech
Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) plus the GSAP plugins CustomEase and SplitText. No smooth-scroll library (the page does not scroll during the intro). Register the plugins with gsap.registerPlugin(CustomEase, SplitText). Fire the whole sequence on DOMContentLoaded.
Layout / HTML
Semantic structure (class names are load-bearing — the JS/CSS query them):
<nav>
<div class="nav-logo"><a href="#">Elara Vandenberg</a></div>
<div class="nav-items">
<a>Runway</a><a>Lookbook</a><a>Campaigns</a><a>Biography</a>
</div>
</nav>
<section class="hero">
<div class="hero-overlay">
<div class="counter"><h1>0</h1></div>
<div class="overlay-text-container">
<div class="overlay-text">
<p>Structure</p>
<p>Designed Identity</p>
<p>Welcome</p>
</div>
</div>
</div>
<div class="hero-images">
<div class="img"><img src="..." alt="" /></div>
<div class="img"><img src="..." alt="" /></div>
<div class="img hero-img"><img src="..." alt="" /></div> <!-- center, index 2 -->
<div class="img"><img src="..." alt="" /></div>
<div class="img"><img src="..." alt="" /></div>
</div>
<div class="hero-header"><h1>Elara Vandenberg</h1></div>
</section>
Notes:
.hero-imagesholds exactly 5.imgwrappers; the third one (index 2) also gets classhero-img— it is the center image that survives and scales to fill.- Use "Elara Vandenberg" as the neutral placeholder brand/name.
Styling
Fonts (Google Fonts): DM Sans (body, all big headings) and DM Mono (nav links + overlay label). Import both.
Palette (CSS vars): --light: #fff, --dark: #0f0f0f. Default text color #000.
Global:
* { margin:0; padding:0; box-sizing:border-box };body { font-family:"DM Sans" }.img { width:100%; height:100%; object-fit:cover }.a, p:text-decoration:none; text-transform:uppercase; font-family:"DM Mono"; font-size:0.9rem; font-weight:500; line-height:1.25; color:#000.
nav: position:fixed; width:100%; padding:2rem; display:flex; justify-content:space-between; align-items:flex-start. .nav-items is a flex-direction:column; align-items:flex-end stack.
.hero: position:relative; width:100%; height:100svh; overflow:hidden.
.hero-overlay (the dark preloader panel):
position:absolute; width:100%; height:100svh; background:var(--dark); z-index:0.- Initial
clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)(full rectangle covering everything).will-change: clip-path. .counterinside it:position:absolute; right:2rem; bottom:2rem; color:var(--light). Itsh1:font-size:4rem; font-weight:500..overlay-text-container:position:absolute; top:2rem; left:2rem; height:2rem; overflow:hidden(a 2rem-tall clipping window — only one line shows at a time)..overlay-text:display:flex; flex-direction:column; transform:translateY(2rem); will-change:transform(starts pushed one line DOWN, so the window is initially empty)..overlay-text p:color:var(--light); height:2rem; display:flex; align-items:center(each line is exactly the window height).
.hero-images:
position:absolute; top:50%; transform:translateY(-50%); width:100%; padding:0 2rem; display:flex; justify-content:center; gap:10vw; z-index:2; will-change:gap..hero-images .img:width:10vw; aspect-ratio:5/7. Initialtransform:translateY(50%) scale(0.5); opacity:0; clip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)(full rect).will-change:opacity, transform, clip-path.
.hero-header:
position:absolute; bottom:2rem; width:100%; z-index:-1(sits BEHIND the overlay, revealed only when the overlay wipes away).h1:text-transform:uppercase; text-align:center; font-size:15vw; font-weight:500; line-height:0.85..hero-header h1 .word(the SplitText word wrappers):transform:translateY(100%); will-change:transform(each word starts hidden below its masked line).
Z-index stack (important for the reveal to read right): header -1 (behind) < overlay 0 < images 2 (front).
GSAP effect (be exact)
Setup
CustomEase.create("hop", "0.85, 0, 0.15, 1");
const counterEl = document.querySelector(".hero-overlay .counter h1");
const heroHeader = document.querySelector(".hero-header h1");
const overlayText= document.querySelector(".overlay-text");
const heroImages = document.querySelector(".hero-images");
const images = gsap.utils.toArray(".hero-images .img"); // length 5
const heroOverlay= document.querySelector(".hero-overlay");
const counter = { value: 0 };
const split = new SplitText(heroHeader, {
type: "words",
mask: "words", // each word gets an overflow-hidden mask wrapper
wordsClass: "word", // matches the CSS translateY(100%) init above
});
Create three independent timelines that run in parallel (each with its own delay):
Timeline 1 — counterTl (delay 0.5)
One tween on the plain object counter:
counter.value: 0 → 100,duration: 5,ease: "power2.out".onUpdate: writeMath.floor(counter.value)intocounterEl.textContent(so the bottom-right number ticks up from 0 to 100 over 5s, decelerating).
Timeline 2 — overlayTextTl (delay 0.75)
Four sequential tweens on overlayText, each duration: 0.75, ease: "hop", stepping the flex column up one line per beat through the 2rem window (label text: Structure → Designed Identity → Welcome → empty):
y: "0"— reveals line 1 ("Structure").y: "-2rem",delay: 0.75— reveals line 2 ("Designed Identity").y: "-4rem",delay: 0.75— reveals line 3 ("Welcome").y: "-6rem",delay: 1— scrolls everything out (empty window again).
Timeline 3 — revealTl (delay 0.5) — the main reveal
Sequential unless a position param says otherwise. All tweens ease: "hop" unless noted.
- Images rise + fade in:
images→{ y: 0, opacity: 1, stagger: 0.05, duration: 1 }. (FromtranslateY(50%) scale(0.5) opacity:0toy:0 opacity:1, still at scale 0.5, left-to-right stagger.) - Gap collapses:
heroImages→{ gap: "0.75vw", duration: 1, delay: 0.5 }(from10vwdown to0.75vw— images pull together into a tight filmstrip). - Images scale up — SIMULTANEOUS with step 2 (position param
"<"):images→{ scale: 1, duration: 1 }(0.5 → 1). - Side images wipe away upward:
images.filter((_, i) => i !== 2)(the 4 non-center images) →{ clipPath: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)", duration: 1, stagger: 0.1 }. The clip-path collapses each image's bottom edge up to the top edge, so they vanish upward one after another, leaving only the center image. - Center image scales to fill:
images[2]→{ scale: 2, duration: 1 }(1 → 2, grows to dominate the frame). - Overlay wipes upward:
heroOverlay→{ clipPath: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)", duration: 1 }. Same collapse-to-top-edge clip-path, so the whole dark preloader panel wipes up and off, uncovering the hero header behind it. - Hero name slides in word by word (position param
"-=0.5", i.e. starts 0.5s before step 6 ends, overlapping the overlay wipe):".hero-header h1 .word"→{ y: "0", duration: 0.75, stagger: 0.1, ease: "power3.out" }. Each word slides up fromtranslateY(100%)into its mask (ease: "power3.out", NOT hop, for this one).
Ease reference
hop=CustomEase.create("hop", "0.85, 0, 0.15, 1")— a steep symmetric in-out used for nearly everything.counterTlusespower2.out; the final word slide usespower3.out.
Assets / images
Five portrait editorial fashion photos, aspect-ratio roughly 5/7, each a single subject against a flat studio or atmospheric backdrop. They are NOT a matched set — the color stories differ per frame, which is fine here since only one survives to full-bleed. What's actually shown, in row order (positions 1–5 = img_1–img_5):
- img_1 (position 1): a woman with shoulder-length brown hair looking off to the side, wearing a deep burgundy/wine wool coat over a matching draped red gown, against a saturated cobalt-blue background.
- img_2 (position 2): a young man with curly dark-brown hair in an olive/moss-green tailored suit and tie, facing camera, against a soft painterly abstract backdrop of warm tan, peach and turquoise brushstrokes.
- img_3 — the center image (index 2, the third) that scales up to fill the screen: a side-profile of a woman with dark hair in a low bun and gold hoop earring, wearing a multicolor brushstroke-print sleeveless top, against a bright orange background. Being the hero frame, its off-center profile still reads well full-bleed.
- img_4 (position 4): a model with a short brown fringe/bob, bare chest under an open bright-red blazer with layered gold necklaces, against a deep teal/petrol-blue background.
- img_5 (position 5): a pale, white-haired woman in a long white Victorian lace dress standing on a grassy islet flanked by two black swans, in a misty lake scene with pine trees and lily pads — muted sage-green, grey and cream tones.
Use object-fit: cover. The four non-center frames are only seen briefly as a tight filmstrip before they wipe away upward, so their differing palettes and moods don't need to match. If you have fewer than five images, repeat one to fill the row.
Behavior notes
- Autoplay once on load; no scroll, hover or click triggers. Total runtime ≈ 7s.
- Responsive (
max-width: 1000px):navpadding →1rem;.counter→right:1rem; bottom:1rem;.hero-images→padding:0 0.5rem; gap:2.5vw;.hero-images .img→width:20vw. Thehero-headerfont at15vwand image scale-up naturally adapt. - Uses
100svh(small viewport height) so mobile browser chrome doesn't clip the hero. will-changehints are set on the overlay clip-path, image transforms/clip-path, the overlay-text transform, the imagesgap, and the header words — keep them, they matter for smoothness of the clip-path and gap animations.