Clip-Path Drop-Down Overlay Menu — Top Panel Wipe + Staggered Link Reveal + Growing Reel Thumbnail + Divider Line Draw
Goal
Build a page with a fixed top navbar (logo + a "Menu" text button) sitting over a full-bleed photographic background. Clicking Menu opens a lime-green overlay panel that drops down from the top edge via an animated clip-path, and clicking Close reverses it. The signature effect is a single paused, reversible GSAP timeline whose four tweens all fire together at time 0: (1) the overlay's clip-path polygon expands from a zero-height top strip to the full panel (wipes down from top); (2) the nav links + CTA button fade up from below with a small stagger; (3) the reel thumbnail grows in height from 0 to 200px; and (4) the footer divider draws itself from 0% to 100% width. Open plays the timeline forward, Close plays the exact same timeline in reverse. It is entirely click-driven — no scroll, no autoplay.
Tech
Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) only — no GSAP plugins, no ScrollTrigger, no smooth-scroll library. Import as:
import gsap from "gsap";
Icons: load Phosphor Icons (web) via its <script> tag in <head> and use one filled play-circle icon glyph (<i class="ph-fill ph-play-circle"></i>). If unavailable, any small play-circle icon substitutes.
Layout / HTML
.container (position: relative, full width/height)
.website-content (the underlying page copy — sits behind everything)
h1 / p ... (a few heading + paragraph blocks of filler copy)
nav (fixed top bar, flex space-between, white text)
.logo "Impulse"
.menu-open-btn "Menu" (click target → open)
.menu-overlay (fixed top green panel; clip-path collapsed closed)
.menu-nav (flex space-between)
.menu-logo "Impulse"
.menu-close-btn "Close" (click target → close)
.menu-cols (flex row of two .col)
.col
.video (width: 50%)
.video-preview (reel thumbnail bg; height 0 closed → grows)
.video-details (flex space-between)
p → <i ph-fill ph-play-circle> "Play reel"
p "-01:18"
.col
.menu-link > a "Home"
.menu-link > a "Workplace"
.menu-link > a "Services & Models"
.menu-link > a "Our Story"
.menu-link > a "Contact"
.btn > a "Take a seat" (CTA button)
.menu-footer
.menu-divider (width 0% → draws to 100%)
.menu-footer-copy (flex space-between)
.slogan > p "Tomorrow's Brands, Today.™"
.socials → a "Twitter" a "Instagram" a "LinkedIn"
Use the neutral, fictional brand name "Impulse" and the labels above — no real brand names.
Styling
Font: "Neue Montreal", sans-serif (a clean neutral grotesque; substitute a similar sans such as Inter/Helvetica if the face is unavailable). Global reset: * { margin:0; padding:0; box-sizing:border-box; }.
Color tokens (CSS custom properties):
--color-menu-overlay: #a0e806(bright lime/chartreuse — the overlay panel fill)--color-menu-text: #000(black — all text/links/icons inside the overlay)--color-divider: #000000(black — the footer divider line)
Structural / load-bearing CSS:
html, body:width:100%; height:100%;background = the hero imageno-repeat 50% 50%,background-size: cover;overflow-x: hidden. Links:text-decoration:none; color: var(--color-menu-text);.i { position: relative; top: 1px; }..container:position: relative; width:100%; height:100%;.nav:position: fixed; top:0; width:100%;flexjustify-content: space-between; padding: 2em; color:#fff; z-index:0;..menu-open-btn { cursor:pointer; }..website-content:position:absolute; top:0; width:100%; margin-top:10em; padding:6em 2em; color:#fff; background: rgba(0,0,0,0.5); z-index:0;.h1 { font-size:80px; font-weight:500; margin-bottom:0.25em; }p { font-size:24px; font-weight:400; }..menu-overlay:position: fixed; top:0; width:100%; padding: 2em;background: var(--color-menu-overlay);pointer-events: none;clip-path: polygon(0 0, 100% 0, 100% 0, 0 0);z-index: 1000 !important;. Note: no explicit height on desktop, so the panel is only as tall as its content (a drop-down, not fullscreen); the closed clip-path is a degenerate zero-height rectangle pinned to the top edge (top two points aty:0, bottom two also aty:0), making the panel invisible until the timeline opens it..menu-close-btn { cursor:pointer; }..menu-nav: flexjustify-content: space-between; margin-bottom: 1em; color: var(--color-menu-text);..menu-cols:display: flex;..menu-cols > div { flex:1; padding: 1em 0; }..video:width: 50%;..video-preview:width:100%; height:0px;background = the reel thumbnail imageno-repeat 50% 50%,background-size: cover; border-radius: 4px;(starts fully collapsed to 0 height)..video-details:width:100%;flexjustify-content: space-between; padding: 0.5em 0; color: var(--color-menu-text);..menu-link:position: relative; width: max-content;..menu-link a { font-size: 40px; }. Hover underline:.menu-link:after { content:""; position:absolute; top:100%; left:0; width:0; height:2px; background: var(--color-menu-text); transition: 0.3s all; }and.menu-link:hover:after { width:100%; }..btn:position: relative; margin: 2em 0; border: 1px solid var(--color-menu-text); width: max-content; padding: 1.25em 2.5em; border-radius: 4px; overflow: hidden; cursor: pointer;. Fill-on-hover:.btn:before { content:""; position:absolute; top:0; left:0; width:0; height:100%; background: var(--color-menu-text); transition: 0.3s all; z-index:-1; },.btn:hover:before { width:100%; },.btn:hover a { color: var(--color-menu-overlay); }(text flips to lime as the black fill sweeps in)..menu-footer: flex column,color: var(--color-menu-text);..menu-divider:width: 0%; height: 1px; background: var(--color-divider); margin: 1em 0;(starts at zero width)..menu-footer-copy: flexjustify-content: space-between;..socials: flexgap: 1em;.
GSAP effect (exhaustive)
1. Build one paused timeline at DOMContentLoaded
Everything runs inside document.addEventListener("DOMContentLoaded", …). Create a single paused timeline; append four tweens, three of them positioned at "<" so they all begin at time 0 together:
let tl = gsap.timeline({ paused: true });
// Tween A — overlay clip-path wipe open (appended at t = 0)
tl.to(".menu-overlay", {
duration: 1,
clipPath: "polygon(0 0, 100% 0, 100% 100%, 0 100%)",
ease: "power2.out",
});
// Tween B — nav links + CTA fade up (position "<" = start of previous tween = t = 0)
tl.from(".menu-link, .btn", {
opacity: 0,
y: 60,
stagger: 0.05,
duration: 0.75,
ease: "power1.inOut",
}, "<");
// Tween C — reel thumbnail grows (position "<" = t = 0)
tl.to(".video-preview", {
duration: 1,
height: "200px",
ease: "power2.out",
}, "<");
// Tween D — footer divider draws (position "<" = t = 0)
tl.to(".menu-divider", {
duration: 2,
width: "100%",
ease: "power4.out",
}, "<");
Exact behavior of each tween:
- A — clip-path panel wipe.
.menu-overlayclip-pathanimates from the degenerate closed polygonpolygon(0 0, 100% 0, 100% 0, 0 0)(zero-height strip at top) to the full rectanglepolygon(0 0, 100% 0, 100% 100%, 0 100%), so the lime panel wipes open downward from the top edge over 1s onpower2.out. - B — links/CTA reveal. A
.from()on all.menu-linkand.btnelements: they animate fromopacity:0, y:60to their natural state (opacity:1, y:0), i.e. they fade in while sliding up 60px.duration: 0.75,ease: "power1.inOut",stagger: 0.05so the six items (5 links + CTA) cascade up 0.05s apart top-to-bottom. Because this is a.from()on a paused timeline sitting at t=0, the items render pre-hidden (opacity 0, pushed down) while the menu is closed. - C — reel thumbnail grow.
.video-previewheightanimates0px → 200pxover 1s onpower2.out, so the thumbnail expands open from a zero-height sliver. - D — divider draw.
.menu-dividerwidthanimates0% → 100%over 2s onpower4.out— a slow line that draws left-to-right and is the last thing to finish (the timeline's total length is 2s, set by this longest tween).
All four start simultaneously at t=0; the timeline runs 0 → ~2s. There is no delay, no labels beyond the "<" position params.
2. Open / close = play / reverse the same timeline
function openMenu() {
document.querySelector(".menu-overlay").style.pointerEvents = "all";
tl.play();
}
function closeMenu() {
document.querySelector(".menu-overlay").style.pointerEvents = "none";
tl.reverse();
}
document.querySelector(".menu-open-btn").addEventListener("click", openMenu);
document.querySelector(".menu-close-btn").addEventListener("click", closeMenu);
tl.reverse(); // prime the timeline in its reversed/closed state at init
- Open: flip the overlay's
pointer-eventstoall(so its links/close button become clickable) andtl.play()— panel wipes down, links fade up, thumbnail grows, divider draws. - Close: set
pointer-eventsback tononeandtl.reverse()— the identical timeline runs backwards: divider retracts, thumbnail collapses, links fade back down, panel wipes up and re-hides. - Init: call
tl.reverse()once at the end. On a paused timeline sitting at time 0 this sets the play direction to reverse and holds it at the start, ensuring the.from()link tween is committed to its hidden start values and the menu begins fully closed.
Assets / images
- 1 hero image — role: *full-bleed page background behind the site content and navbar* (set on
body,background-size: coverat50% 50%). A moody, atmospheric photographic backdrop (dim/low-key so the white nav text and the semi-transparent dark content block read on top). Landscape, ~16:9 (e.g. 1600×900 or larger). - 1 reel thumbnail — role: *background of the
.video-previewpanel inside the open overlay* (background-size: cover,50% 50%,border-radius: 4px). A single video-still / showreel frame; it renders inside a half-width column and grows to 200px tall, so a landscape ~16:9 still works best.
No logos or icon images are needed beyond the Phosphor play-circle glyph; the "Impulse" logo and slogan are plain text.
Behavior notes
- Entirely click-driven — no scroll, no ScrollTrigger, no autoplay, no loops. One paused, reversible timeline is the single source of truth for open/close.
- The closed overlay is
pointer-events: none(never blocks the page);pointer-eventsis toggled toallon open so only the open menu is interactive. - Responsive (
max-width: 900px):.menu-overlaygetsheight: 100vh(fullscreen on mobile instead of a content-height drop-down);.menu-colsswitches todisplay: block(columns stack);.video-previewgetsheight: 125px;.menu-link ashrinks tofont-size: 30px. - Hover niceties (pure CSS, not GSAP): each
.menu-linkgrows a 2px underline left-to-right; the.btnfills black from the left while its text flips to lime.
</content> </invoke>