All components

Rejoice Fullscreen Overlay Menu

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

Open live demo ↗ Raw prompt (.md)

What it does

A fullscreen overlay navigation menu that opens on clicking the 'Menu' button and closes on 'Close'. A single paused GSAP timeline plays/reverses: it animates the overlay clip-path open, staggers the nav links and CTA up with a fade, grows the video preview thumbnail and draws the footer divider from 0 to 100% width.

How it's built

Categorymenu
Techgsap
Complexitypage
Performance costlight
Mobile-safeyes

menu fullscreen-overlay clip-path gsap-timeline staggered-reveal hamburger navigation vanilla-js minimal

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

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 image no-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%; flex justify-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 at y:0, bottom two also at y:0), making the panel invisible until the timeline opens it. .menu-close-btn { cursor:pointer; }.
  • .menu-nav: flex justify-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 image no-repeat 50% 50%, background-size: cover; border-radius: 4px; (starts fully collapsed to 0 height). .video-details: width:100%; flex justify-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: flex justify-content: space-between;. .socials: flex gap: 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-overlay clip-path animates from the degenerate closed polygon polygon(0 0, 100% 0, 100% 0, 0 0) (zero-height strip at top) to the full rectangle polygon(0 0, 100% 0, 100% 100%, 0 100%), so the lime panel wipes open downward from the top edge over 1s on power2.out.
  • B — links/CTA reveal. A .from() on all .menu-link and .btn elements: they animate from opacity:0, y:60 to their natural state (opacity:1, y:0), i.e. they fade in while sliding up 60px. duration: 0.75, ease: "power1.inOut", stagger: 0.05 so 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-preview height animates 0px → 200px over 1s on power2.out, so the thumbnail expands open from a zero-height sliver.
  • D — divider draw. .menu-divider width animates 0% → 100% over 2s on power4.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-events to all (so its links/close button become clickable) and tl.play() — panel wipes down, links fade up, thumbnail grows, divider draws.
  • Close: set pointer-events back to none and tl.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: cover at 50% 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-preview panel 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-events is toggled to all on open so only the open menu is interactive.
  • Responsive (max-width: 900px): .menu-overlay gets height: 100vh (fullscreen on mobile instead of a content-height drop-down); .menu-cols switches to display: block (columns stack); .video-preview gets height: 125px; .menu-link a shrinks to font-size: 30px.
  • Hover niceties (pure CSS, not GSAP): each .menu-link grows a 2px underline left-to-right; the .btn fills black from the left while its text flips to lime.

</content> </invoke>