All components

Responsive Fullscreen Menu

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

Open live demo ↗ Raw prompt (.md)

What it does

A fullscreen overlay menu that opens on clicking 'Menu': the panel sweeps up via an animated clip-path polygon on a power4.inOut ease while the hero slides away, then the logo, oversized nav links and footer lines reveal with staggered power3.out y-translations. A stack of four semi-transparent images parallaxes and tilts in 3D (rotate3d) following the mouse via GSAP tweens.

How it's built

Categorymenu
Techgsap
Complexitysection
Performance costlight
Mobile-safedesktop-first

fullscreen-menu overlay clip-path parallax 3d-tilt staggered-reveal gsap editorial

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

Responsive Fullscreen Menu — Clip-Path Sweep-Up + Staggered Reveal + 3D Parallax Image Stack

Goal

Build a minimal editorial landing page with a tiny fixed top navbar (a small logo mark on the left, a "Menu" label on the right) sitting over a full-bleed hero photo with an oversized display heading. Clicking "Menu" opens a fullscreen dark overlay menu: the dark panel sweeps up from the bottom edge by animating a clip-path polygon on a power4.inOut ease while the hero photo simultaneously slides up and fades away. Once the panel is up, the menu logo, four oversized nav links, and two footer columns of tiny mono lines rise and reveal from behind clip-path masks with staggered power3.out y-translations. On the wide left side of the open menu sits a stack of four semi-transparent copies of the hero image that parallax and tilt in 3D (rotate3d) continuously as the mouse moves — each layer offset by a different amount for depth. Clicking "Close" reverses it: the panel collapses off the top edge, the menu column slides up and fades, and the hero photo slides back up into view from below; on complete everything hard-resets for the next open. All motion is click/mousemove-driven with plain GSAP tweens (no timeline).

Tech

Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) only — no plugins (no ScrollTrigger, no SplitText, no CustomEase), no smooth-scroll library. The whole thing is click- and mousemove-driven. Import as:

import gsap from "gsap";

Ship index.html, styles.css, and an ES-module script.js (<script type="module" src="./script.js">). Wrap logic in a DOMContentLoaded listener (the file uses two separate DOMContentLoaded handlers — one for open/close, one for the mouse tilt — but a single one is fine).

Layout / HTML

Use neutral, fictional labels — no real brand names. The demo wordmark/heading is "Break".

.container
  nav                                   (fixed top bar, space-between)
    .logo > img                         (small logo mark, left)
    p.menu-open  "Menu"                 (right — THE open click target)

  section.hero                          (fullscreen background photo)
    .header
      h1  "Break"                       (giant display heading, centered)
      sup  ©                            (small superscript copyright next to it)

  .menu                                 (fixed fullscreen dark overlay, clipped shut, pointer-events:none)
    .menu-nav > p.menu-close  "Close"   (fixed top-right — THE close click target)

    .menu-col.menu-img                  (wide left column — the 3D image stack)
      img#img-1                         (four copies of the SAME hero photo,
      img#img-2                          descending opacity 0.9 / 0.7 / 0.5 / 0.3)
      img#img-3
      img#img-4

    .menu-col.menu-items                (narrow right column)
      .menu-logo > img                  (logo mark, larger)
      .menu-links
        .menu-link > p > a  "About"
        .menu-link > p > a  "Story"
        .menu-link > p > a  "Projects"
        .menu-link > p > a  "Releases"
      .menu-footer                      (two flex sub-columns of tiny mono lines)
        .menu-sub-col
          .menu-sub-item > p  ×N   (labels + a fictional address block, <br/> separators)
        .menu-sub-col
          .menu-sub-item > p  ×N   (socials + a second fictional address block)

Footer copy is fictional filler, e.g. col 1: Connect / Break / — / Explore / Break / — / Contact Us / — / Ocean City / Rhythmic Sound / 2000 Coastal Hwy / Ocean City, MD 21842; col 2: Instagram / Youtube / — / Spotify / Mixcloud / — / Email / — / Vice City / Echo Beats / 933 South View Ave / Vice City, VC 3270. Use <br /> for the blank separator lines. All four .menu-img <img> use the same hero photo src.

Styling

Fonts (name these families; supply close free substitutes if the originals aren't available):

  • Display / links: "Saans TRIAL" — a clean neutral grotesque sans (substitute: Inter / Helvetica Now / a general grotesque). Used on .hero h1 and .menu-link p.
  • Mono / labels: "Akkurat Mono" — a monospace (substitute: Space Mono / Roboto Mono). Used on all <p> (nav "Menu"/"Close", footer lines).

Global reset: * { margin:0; padding:0; box-sizing:border-box; }. html, body { width:100%; height:100%; font-family:"Saans TRIAL"; background:#000; }. img { width:100%; height:100%; object-fit:cover; }. Base p { color:#fff; font-family:"Akkurat Mono"; font-size:11px; font-weight:300; text-transform:uppercase; }.

Color tokens: page/hero text white #fff; page background #000; overlay menu background dark #101010.

Type sizes (large / load-bearing):

  • .hero h1: white, uppercase, Saans, font-size: 12vw, font-weight: 900, letter-spacing: -0.024em, line-height: 100%.
  • sup: white, font-size: 3vw, position: relative; top: 0.25em; (sits beside the h1 in a flex .header with gap: 0.5em).
  • .menu-link p: Saans, font-size: 36px, font-weight: 700. .menu-link a { text-decoration:none; color:#fff; }.
  • .logo: width: 24px; · .menu-logo: width: 48px;.

Structural / load-bearing CSS:

  • .hero: position: absolute; top: 0; width: 100vw; height: 100vh; overflow: hidden; z-index: 0; with the hero photo as its background (background: url(hero.jpg) no-repeat 50% 50%; background-size: cover;). .hero .header is position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); display:flex; gap:0.5em;.
  • nav: position: fixed; top: 0; padding: 1.5em 2em; width: 100%; display: flex; justify-content: space-between; align-items: center; z-index: 1;. .menu-open, .menu-close { cursor: pointer; }.
  • .menu: position: fixed; top: 0; padding: 1.5em; width: 100vw; height: 100vh; background: #101010; color: #fff; display: flex; gap: 1.5em; overflow: hidden; z-index: 2; pointer-events: none; and the collapsed initial clip-path clip-path: polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%); (a zero-height line pinned to the bottom edge, so the panel is invisible at rest).
  • .menu-nav: position: fixed; top: 0; left: 0; padding: 2em; width: 100%; display: flex; justify-content: flex-end; (the "Close" label sits top-right).
  • .menu-img: position: relative; flex: 3; overflow: hidden; (wide left column, ~75%). overflow:hidden clips the tilted/parallaxing image stack.
  • .menu-items: position: relative; top: 0px; flex: 1; padding: 3em; display: flex; flex-direction: column; justify-content: space-between; z-index: -1; (narrow right column, ~25%).
  • .menu-img img: position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); — all four stacked and centered. Per-image opacity: #img-1 {0.9}, #img-2 {0.7}, #img-3 {0.5}, #img-4 {0.3}.
  • Clip-path masks (these make the staggered text/logo reveal read as a slide-up-from-behind-a-mask): .menu-logo, .menu-link, and .menu-sub-item each get clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%); (a full rectangle acting as an overflow mask). Their inner elements (.menu-logo img, .menu-link p, .menu-sub-item p) are position: relative; so a positive y offset hides them below the mask edge.
  • .menu-footer: width: 100%; display: flex;. .menu-sub-col { flex: 1; }.

GSAP effect (exhaustive)

Default ease constant: const defaultEase = "power4.inOut";. No gsap.timeline anywhere — every step is an independent gsap.to/gsap.set, and the choreography comes purely from delay and stagger values. A single boolean isOpen (flipped inside the tweens' onComplete) gates re-clicks.

0. Initial hidden states (set once on load)
gsap.set(".menu-logo img", { y: 50 });
gsap.set(".menu-link p",   { y: 40 });
gsap.set(".menu-sub-item p", { y: 12 });
gsap.set(["#img-2, #img-3, #img-4"], { top: "150%" });   // #img-1 stays at top:50%

So the logo, each link, and each footer line start pushed down behind their clip-path masks (hidden), and images 2–4 start pushed below the viewport (top:150%) while #img-1 sits centered.

1. OPEN — clicking .menu-open runs openMenu() (six parallel tweens)

All start at the click (t = 0); the delays are absolute offsets from that click.

  1. Panel sweep-upgsap.to(".menu", { clipPath: "polygon(0% 100%, 100% 100%, 100% 0%, 0% 0%)", pointerEvents: "all", duration: 1.25, ease: defaultEase }). The clip-path goes from the bottom-collapsed line to a full rectangle, so the dark panel wipes upward to fill the screen; pointer events are enabled.
  2. Hero slides up + fadesgsap.to(".hero", { top: "-50%", opacity: 0, duration: 1.25, ease: defaultEase }). The background photo pushes up off the top and fades to 0 as the panel covers it.
  3. Menu logo risesgsap.to(".menu-logo img", { y: 0, duration: 1, delay: 0.75, ease: "power3.out" }). Slides from y:50 up into its clip-mask.
  4. Nav links rise (staggered)gsap.to(".menu-link p", { y: 0, duration: 1, stagger: 0.075, delay: 1, ease: "power3.out" }). The 4 links slide from y:40 up into view, 0.075s apart.
  5. Footer lines rise (staggered)gsap.to(".menu-sub-item p", { y: 0, duration: 0.75, stagger: 0.05, delay: 1, ease: "power3.out" }). All footer mono lines slide from y:12 up, a tighter 0.05s apart.
  6. Image stack slides in (staggered)gsap.to(["#img-2, #img-3, #img-4"], { top: "50%", duration: 1.25, ease: defaultEase, stagger: 0.1, delay: 0.25, onComplete: () => { gsap.set(".hero", { top: "50%" }); isOpen = !isOpen; } }). Images 2–4 rise from top:150% to the centered top:50%, 0.1s apart, layering behind #img-1. On complete it parks the (already invisible) hero at top:50% and flips isOpen true.

Net feel: the dark panel sweeps up over the fading hero (0→1.25s); starting ~0.75–1s in, the logo, links, and footer lines stagger up from behind their masks while the four image layers rise into the stack.

2. CLOSE — clicking .menu-close runs closeMenu() (three tweens + a reset)
  1. Panel collapse off topgsap.to(".menu", { clipPath: "polygon(0% 0%, 100% 0%, 100% 0%, 0% 0%)", pointerEvents: "none", duration: 1.25, ease: defaultEase }). The clip-path collapses to a zero-height line at the top edge, so the dark panel wipes up and off; pointer events disabled.
  2. Menu column exits up + fadesgsap.to(".menu-items", { top: "-300px", opacity: 0, duration: 1.25, ease: defaultEase }). The whole right column slides up 300px and fades.
  3. Hero slides back in + fades ingsap.to(".hero", { top: "0%", opacity: 1, duration: 1.25, ease: defaultEase, onComplete: () => { /* full reset */ } }). The hero animates from top:50% (parked below) back up to top:0 while fading from 0→1, re-revealing the landing photo from below. Its onComplete hard-resets everything for the next open:

``js gsap.set(".menu", { clipPath: "polygon(0% 100%, 100% 100%, 100% 100%, 0% 100%)" }); // re-arm at bottom gsap.set(".menu-logo img", { y: 50 }); gsap.set(".menu-link p", { y: 40 }); gsap.set(".menu-sub-item p", { y: 12 }); gsap.set(".menu-items", { opacity: 1, top: "0px" }); gsap.set(["#img-2, #img-3, #img-4"], { top: "150%" }); isOpen = !isOpen; ``

3. Click wiring + re-entry guard
menuOpen.addEventListener("click", () => { if (isOpen) return; openMenu(); });
menuClose.addEventListener("click", () => { if (!isOpen) return; closeMenu(); });

isOpen starts false and is only flipped inside the tweens' onComplete, so a second click is ignored until the current open/close fully finishes.

4. Continuous 3D parallax + tilt on the image stack (mousemove)

Independent of the open/close logic. Track the mouse against the viewport center:

const menuImgContainer = document.querySelector(".menu-img");
const images = document.querySelectorAll(".menu-img img");
let mouse = { x: 0, y: 0 };
let cx = window.innerWidth / 2, cy = window.innerHeight / 2;
const scales = [0.81, 0.84, 0.87, 0.9];   // per-layer scale (img-1..img-4)

On every mousemove on document.body, store mouse.x/​y = event.clientX/​Y and call update():

function update() {
  let dx = mouse.x - cx, dy = mouse.y - cy;
  let tiltx = (dy / cy) * 20;             // up to ±20 on the X axis
  let tilty = (dx / cx) * 20;             // up to ±20 on the Y axis

  gsap.to(menuImgContainer, {
    duration: 2,
    transform: `rotate3d(${tiltx}, ${tilty}, 0, 15deg)`,   // whole stack tilts 15° about a mouse-driven axis
    ease: "power3.out",
  });

  images.forEach((img, index) => {
    let parallaxX = -(dx * (index + 1)) / 100;   // deeper layers move MORE
    let parallaxY = -(dy * (index + 1)) / 100;
    gsap.to(img, {
      duration: 2,
      transform: `translate(calc(-50% + ${parallaxX}px), calc(-50% + ${parallaxY}px)) scale(${scales[index]})`,
      ease: "power3.out",
    });
  });
}
  • The .menu-img container is rotated in 3D via rotate3d(tiltx, tilty, 0, 15deg) — a fixed 15° tilt whose axis is steered by the mouse offset, so the stack appears to lean toward/away from the cursor.
  • Each of the four images gets a parallax translate on top of its translate(-50%,-50%) centering, with the offset scaled by (index+1) — so #img-1 barely moves and #img-4 moves ~4× as far, producing depth. Each layer is also held at its own scale (0.81→0.9, front layer largest).
  • Both tweens use duration: 2 + ease: "power3.out", so the stack drifts smoothly and lags behind the cursor (a heavy, floaty parallax). resize recomputes cx/cy.

Assets / images

  • 1 full-bleed editorial photograph — landscape orientation (~3:2). Role: (a) the fullscreen hero background behind the closed state, and (b) reused four times as the stacked, semi-transparent (0.9 / 0.7 / 0.5 / 0.3 opacity) images inside the open menu's left column. A neutral, well-lit studio subject on a plain light background reads well; displayed object-fit: cover / background-size: cover at 50% 50%. All four stack <img> share this one source.
  • 1 small logo mark (PNG, transparent background) — a simple white brand mark. Role: top-left in the fixed navbar (24px wide) and again inside the open menu (48px wide).

Behavior notes

  • Re-entry guard: the isOpen flag (toggled only in onComplete) blocks new clicks until the current open/close finishes; there is no timeline to reverse — open and close are separate tween sets.
  • Responsive (max-width: 900px): .menu-img is display: none — the 3D image stack is hidden on mobile and the open menu shows only the links/footer column. (The mousemove parallax still runs but has nothing visible to move.) Treat this as a desktop-first effect.
  • All motion is click-driven (open/close) plus a continuous mousemove parallax loop — no scroll, no ScrollTrigger, no autoplay, no infinite timeline.