Landing Page Reveal — Portrait-Strip Preloader Wipe-Up
Goal
Build a full-screen editorial landing hero with a cinematic preloader-to-hero reveal that plays automatically once on page load (~6.5 seconds total). A black full-screen loader holds a horizontal strip of seven tall portrait images (six photos plus a centered white monogram logo). On load the seven images rise up from below and stagger in, then the whole strip slides sideways; the six photos (but not the center logo) wipe away upward one after another via an animated clip-path; the entire black loader panel then wipes upward the same way, uncovering the page beneath; finally the nav links, the three-line hero headline, and the four footer thumbnails all slide up and fade in. Everything is driven by one single GSAP timeline using power3.inOut easing.
Tech
Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) only — no GSAP plugins and no smooth-scroll library (the page never scrolls; body is overflow:hidden). Import the default export (import gsap from "gsap") and fire the whole sequence inside a DOMContentLoaded listener.
Layout / HTML
Semantic structure (class names / the #loader-logo id are load-bearing — the JS/CSS query them):
<div class="container">
<!-- fixed black preloader on top of everything -->
<div class="loader">
<div class="loader-imgs">
<div class="img"><img src="..." alt="" /></div> <!-- photo 1 -->
<div class="img"><img src="..." alt="" /></div> <!-- photo 2 -->
<div class="img"><img src="..." alt="" /></div> <!-- photo 3 -->
<div class="img" id="loader-logo"><img src="logo.png" alt="" /></div> <!-- CENTER logo, survives the wipe -->
<div class="img"><img src="..." alt="" /></div> <!-- photo 4 -->
<div class="img"><img src="..." alt="" /></div> <!-- photo 5 -->
<div class="img"><img src="..." alt="" /></div> <!-- photo 6 -->
</div>
</div>
<!-- the page underneath, revealed when the loader wipes away -->
<div class="website-content">
<nav>
<div class="nav-item"><a href="#">( menu )</a></div>
<div class="nav-item" id="logo"><a href="#">EliteWebsites</a></div>
<div class="nav-item"><a href="#">( work* )</a></div>
</nav>
<div class="hero">
<div class="h1"><h1>we believe</h1></div>
<div class="h1"><h1>websites shape the</h1></div>
<div class="h1"><h1><span>future</span></h1></div>
</div>
<footer>
<div class="item"><img src="..." alt="" /></div>
<div class="item"><img src="..." alt="" /></div>
<div class="item"><img src="..." alt="" /></div>
<div class="item"><img src="..." alt="" /></div>
</footer>
</div>
</div>
Notes:
.loader-imgsholds exactly 7.imgwrappers. The 4th one (the center) carriesid="loader-logo"and holds the monogram logo — it is the one image that is excluded from the photo wipe and stays visible until the whole loader panel wipes away..website-contentholds the nav, the hero, and the footer, and sits underneath the loader.- Use "EliteWebsites" as the neutral placeholder brand name in the center nav slot. The three headline lines are the tagline "we believe / websites shape the / future" — the word "future" is wrapped in a
<span>so it can be styled bolder.
Styling
Fonts: the CSS declares custom display faces — "Sharp Grotesk" (body + the future span), "Neue Montreal" (nav), "PP Editorial Old" (center nav brand), "PP Migra" (hero headline). None of these are bundled or loaded (there is no @font-face and no font <link>), so in practice everything falls back to the browser default sans-serif. Keep the declared font-family names for fidelity but do not attempt to fetch the missing fonts.
Palette:
- Page background:
#e7e4e5(warm light grey). - Loader background:
#000(black). - All text:
#000.
Global:
* { margin:0; padding:0; box-sizing:border-box }.html, body { font-family:"Sharp Grotesk"; background:#e7e4e5; overflow:hidden }(no scrolling).img { width:100%; height:100%; object-fit:cover }.a { text-decoration:none; text-transform:uppercase; color:#000 }.
Key elements (positioning + the clip-paths and initial states the animation depends on):
.loader:position:fixed; width:100vw; height:100vh; background:#000; pointer-events:none. Initialclip-path: polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)(full rectangle — covers the whole viewport). This exact 4-point full-rect clip-path is required so GSAP can later animate it to a collapsed polygon..loader-imgs:width:150%; position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); display:flex; gap:50px; same full-rectclip-path. It is 150% wide, centered, so the strip overflows the viewport on both sides..img:position:relative; flex:1(seven equal columns filling the 150% strip); same full-rectclip-pathon each.nav:width:100%; padding:30px; display:flex; align-items:center; font-family:"Neue Montreal"..nav-item:position:relative; flex:1(three equal thirds)..nav-item:nth-child(2)(the brand):text-align:center; font-family:"PP Editorial Old"; font-size:24px; font-weight:500..nav-item:nth-child(3):text-align:right.#logo a { text-transform:none }(brand name not uppercased)..hero:width:100%; position:absolute; top:45%; transform:translateY(-50%); text-transform:uppercase; font-family:"PP Migra"..h1:width:100%; text-align:center; font-size:4vw; same full-rectclip-pathon each line.h1:position:relative; font-weight:400; line-height:85%.h1 span:font-family:"Sharp Grotesk"; font-weight:700(the word "future" renders heavier).footer:width:100%; position:absolute; bottom:30px; right:30px; display:flex; justify-content:flex-end; gap:10px; same full-rectclip-path..item:position:relative; width:100px; height:100px(four square thumbnails, bottom-right,object-fit:cover).
GSAP effect (be exact)
Initial states — set BEFORE building the timeline
gsap.set(".img", { y: 500 }); // all 7 strip images pushed 500px DOWN
gsap.set(".loader-imgs",{ x: 500 }); // whole strip shifted 500px to the RIGHT
gsap.set(".nav-item", { y: 25, opacity: 0 }); // nav items 25px down, invisible
gsap.set("h1, .item, footer", { y: 200 }); // hero headlines, footer thumbs AND the footer container 200px down
(The full-rect clip-path initial states come from the CSS above, not from gsap.set.)
One single timeline, delayed 1s
const tl = gsap.timeline({ delay: 1 });
The whole sequence waits 1s after DOMContentLoaded, then plays as a chain of five tweens. Position parameters are relative offsets ("-=X" = start X seconds before the current end of the timeline) — reproduce them exactly.
Tween 1 — images rise (starts at timeline t=0):
tl.to(".img", {
y: 0,
duration: 1.5,
stagger: 0.05,
ease: "power3.inOut",
});
All 7 .img wrappers rise from y:500 to y:0, left-to-right stagger 0.05s, each 1.5s, power3.inOut. (Strip fills from t≈0 to t≈1.8.)
Tween 2 — strip slides sideways (position "-=2.5"):
tl.to(".loader-imgs", {
x: 0,
duration: 3,
ease: "power3.inOut",
}, "-=2.5");
The whole strip slides from x:500 to x:0 (moves left ~500px) over 3s. Position "-=2.5" computes to a negative insertion point (GSAP clamps it to 0), so this runs in parallel with the rising images, starting at t≈0 and ending at t≈3.
Tween 3 — the six photos wipe away upward (position "-=1"):
tl.to(".img:not(#loader-logo)", {
clipPath: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)",
duration: 1,
stagger: 0.1,
ease: "power3.inOut",
}, "-=1");
Every .img except the center #loader-logo (i.e. the 6 photos) animates its clip-path from the full rectangle to polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%) — the bottom edge collapses up to the top edge, so each photo wipes away upward, staggered 0.1s, 1s each. Starts at t≈2. The center logo stays visible on the black panel.
Tween 4 — the whole black loader wipes upward (position "-=0.5"):
tl.to(".loader", {
clipPath: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)",
duration: 1,
ease: "power3.inOut",
}, "-=0.5");
The entire black .loader panel (still carrying the center logo) collapses its bottom edge up to the top the same way — wiping the whole preloader upward off the screen over 1s, uncovering .website-content beneath. Starts at t≈3.
Tween 5 — page content slides up + fades in (position "-=0.5"):
tl.to(".nav-item, h1, footer, .item", {
y: 0,
opacity: 1,
stagger: 0.1,
duration: 1,
ease: "power3.inOut",
}, "-=0.5");
All nav items (3), all hero headline h1s (3), the footer container, and the footer thumbnails (4) — 11 targets total — slide from their offset (y:200, or y:25 for nav) to y:0 and fade opacity 0→1 (only the nav items were transparent), staggered 0.1s, 1s each. Starts at t≈3.5, ends at t≈5.5.
Timeline summary (timeline-relative seconds; add the 1s delay for wall-clock)
| t (s) | what | |-------|------| | 0.0–1.8 | 7 strip images rise y:500→0 (stagger 0.05, dur 1.5) | | 0.0–3.0 | strip slides x:500→0 (dur 3) — parallel | | 2.0–3.5 | 6 photos wipe upward via clip-path (stagger 0.1, dur 1) | | 3.0–4.0 | whole black loader wipes upward via clip-path (dur 1) | | 3.5–5.5 | nav + hero h1s + footer + thumbs slide up & fade in (stagger 0.1, dur 1) |
Total runtime ≈ 6.5s (including the 1s start delay). Every tween uses ease: "power3.inOut".
Ease reference
- Everything uses
power3.inOut. There is no CustomEase, SplitText, ScrollTrigger, lerp loop, or Three.js in this component — it is a pure load-triggered timeline ofy,x,clip-pathandopacitytweens.
Assets / images
Thirteen image slots total: 6 tall portrait photos + 1 portrait logo in the loader strip, and 4 square thumbnails in the footer.
- Loader strip photos (6) — tall portrait ~2:3 editorial/cinematic frames,
object-fit:cover, each a single subject with a moody, atmospheric palette (mixed teals, golds, greens). Roughly, in strip order: (1) a lone figure walking a wet reflective floor toward a shaft of light between dark angular walls; (2) extreme close-up of a glossy helmet with a swirled gold visor on lime-green; (3) a spacesuited figure before a huge pale sphere in hazy golden light; then the center logo; (4) a dark slit-view of a lower face holding a lit cigarette; (5) an orange-lit silhouette pressing a hand on frosted glass; (6) a side-lit portrait of a woman in dark clothing on a dark teal backdrop. They are only briefly seen before wiping away, so the palettes need not match. - Center logo (
#loader-logo) — an ornate white cursive monogram letter on a solid black background, portrait ~3:4. This is the one image that stays visible while the six photos wipe, then leaves with the black panel. Keep it on a black field so it blends into the loader. - Footer thumbnails (4) — small square images (cropped to 100×100 via
object-fit:cover), bottom-right. Any editorial/3D-render/portrait subjects work (e.g. a surreal metallic-faced 3D figure, a golden lucky-cat figurine in a red niche, a veiled portrait, a warm hazy city skyline). Do not use real brand marks.
If you have fewer fixtures than slots, repeat images to fill; if more, use the first ones.
Behavior notes
- Autoplay once on
DOMContentLoaded; no scroll, hover, or click triggers. The page never scrolls (overflow:hidden). - The clip-path target
polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)is the "wipe upward" shape (bottom edge collapsed to the top). The full-rectangle initial clip-paths on.loader,.loader-imgs,.img,.h1andfootermust be present in CSS so GSAP can tween between matching 4-point polygons. - No
will-changehints, no reduced-motion handling, and no responsive breakpoints in the original — it renders the same at all widths (the loader strip is150%wide and centered, so it always overflows). Layout is light and mobile-safe.