All components

Fullscreen Portfolio Menu

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

Open live demo ↗ Raw prompt (.md)

What it does

A fullscreen overlay navigation that toggles open on clicking the burger button: a GSAP timeline sweeps a clip-path polygon over the whole viewport on a power4.inOut ease, then staggers the oversized menu items up from a y offset with power4.out, grows a CSSRulePlugin-animated underline on the active item, and fades the social sub-nav up into place. Clicking again reverses the same timeline.

How it's built

Categorymenu
Techgsap
GSAP pluginsCSSRulePlugin
Complexitysection
Performance costlight
Mobile-safeyes

fullscreen-menu overlay clip-path burger-toggle staggered-reveal cssruleplugin gsap editorial 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

Fullscreen Portfolio Menu — Clip-Path Overlay with Staggered Giant Links

Goal

Build a fullscreen overlay navigation for an editorial portfolio site. A burger button in the top-right toggles the menu: a single GSAP timeline sweeps a dark clip-path overlay down over the entire viewport, then staggers three oversized menu words up from behind a mask, grows a colored underline bar across the active item (animated via CSSRulePlugin on a ::after pseudo-element), and fades a social sub-nav up into place. Clicking the burger again plays the exact same timeline in reverse. The burger morphs into an X. The top nav uses mix-blend-mode: difference so its text/lines invert against whatever is behind them.

Tech

Vanilla HTML/CSS/JS with ES module imports. Use gsap (npm) plus the GSAP plugin CSSRulePlugin (this is required — the active-item underline is a CSS ::after pseudo-element and can only be tweened through CSSRulePlugin.getRule(...)).

import gsap from "gsap";
import { CSSRulePlugin } from "gsap/CSSRulePlugin";
gsap.registerPlugin(CSSRulePlugin);

Wrap all JS in a DOMContentLoaded listener. No smooth-scroll library, no ScrollTrigger, no canvas/WebGL.

Layout / HTML

<div class="website-content">
  <div class="hero-img"><img src="./hero.jpg" alt="" /></div>
</div>

<nav>
  <div class="info"><p>Folio Vol. 1</p></div>
  <div class="logo"><p><a href="#">The Elite Portfolio</a></p></div>
  <div class="toggle-btn">
    <button class="burger" onclick="this.classList.toggle('active');"></button>
  </div>
</nav>

<div class="overlay">
  <div class="overlay-menu">
    <div class="menu-item"><div class="menu-item-name"><p id="active"><a href="#">Index</a></p></div></div>
    <div class="menu-item"><div class="menu-item-name"><p><a href="#">Work</a></p></div></div>
    <div class="menu-item"><div class="menu-item-name"><p><a href="#">About</a></p></div></div>
  </div>
  <div class="sub-nav">
    <p><a href="#">Twitter</a></p><p>·</p>
    <p><a href="#">Instagram</a></p><p>·</p>
    <p><a href="#">Dribble</a></p><p>·</p>
    <p><a href="#">Behance</a></p>
  </div>
</div>

<script type="module" src="./script.js"></script>

Notes:

  • The burger <button> has an inline onclick="this.classList.toggle('active');" that toggles the X shape via CSS — keep it. The JS adds a *separate* click listener on .burger that drives the GSAP timeline.
  • The first menu item's <p> carries id="active" — its ::after is the animated underline.
  • Stacking order (via z-index): .website-content = 0 (base page), .overlay sits above it, nav = 2 (always on top).

Styling

Reset: * { margin: 0; padding: 0; box-sizing: border-box; }. html, body { width: 100%; height: 100%; }.

Palette (exact):

  • #cdc6be — warm greige / off-white (page background, most text, links)
  • #141412 — near-black (overlay background, hero border)
  • #fff — white (menu text, burger lines)
  • #c03f13 — burnt orange-red (the active-item underline bar)

Fonts (by role): the small UI text (info, links) uses a clean neutral grotesque — name it "Neue Montreal" with a sans-serif fallback (substitute Inter/Helvetica Neue). The logo, the giant menu words and the sub-nav use an elegant high-contrast display serif — name it "Canopee" with a serif fallback (substitute any refined display serif such as Cormorant/Playfair if unavailable). The named families can stay declared even without @font-face; the layout is what matters.

Key rules:

  • a { text-decoration: none; color: #cdc6be; }
  • .website-contentposition: fixed; inset: 0; width/height 100%; z-index: 0; background: #cdc6be; padding: 2em;
  • .hero-imgmargin-top: 4em; width: 100%; height: 100%; border: 2px solid #141412; and its img { width:100%; height:100%; object-fit: cover; }
  • navposition: fixed; width: 100%; display: flex; justify-content: space-between; align-items: center; padding: 1.5em 2em; color: #cdc6be; mix-blend-mode: difference; z-index: 2;. nav > div { flex: 1; }. The mix-blend-mode: difference is essential — it makes the nav text/burger invert against the light page and the dark overlay.
  • .logotext-align: center; font-family: "Canopee";. .logo p { position: relative; top: 0.225em; }. .logo a { font-size: 30px; color: #cdc6be; } (drops to 20px under @media (max-width: 900px)).
  • .toggle-btn { display: flex; justify-content: flex-end; }
  • .burgerdisplay: flex; justify-content: center; align-items: center; padding: 1.75em 2em 1.5em 2em; background: rgba(255,255,255,0); border-radius: 0.25em; outline: none; height: 20px; width: 28px; border: none; cursor: pointer; transition: all 250ms ease-out;
  • .burger:before, .burger:aftercontent: ""; width: 40px; height: 2px; position: absolute; background: #fff; transition: all 250ms ease-out; will-change: transform;. :before { transform: translateY(-3px); }, :after { transform: translateY(3px); }. When .burger.active: :before { transform: translateY(0) rotate(45deg); }, :after { transform: translateY(0) rotate(-45deg); } — the two bars cross into an X.
  • .overlayposition: fixed; inset: 0; width: 100vw; height: 100vh; display: flex; background: #141412; clip-path: polygon(0 0, 100% 0, 100% 0, 0 0); will-change: transform;. The initial clip-path is collapsed flat against the top edge (zero height) so the overlay is invisible until animated.
  • .overlay-menuposition: fixed; inset: 0; width: 100vw; height: 100vh; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 1em; color: #fff;
  • .menu-itemdisplay: flex; cursor: pointer; clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);. This rectangular clip-path is the mask that hides each word while it sits pushed down (y: 225) so it can rise into view.
  • .menu-item pposition: relative; text-align: center; font-family: "Canopee"; font-size: 15vw; line-height: 80%; will-change: transform; transition: letter-spacing 0.3s;. On :hover, letter-spacing: 0.075em;.
  • .menu-item p#active::aftercontent: ""; position: absolute; top: 45%; left: 0; transform: translateY(-50%); background: #c03f13; width: 0%; height: 12px;. Starts at width: 0% — GSAP grows it to 100%.
  • .menu-item a { color: #cdc6be; }
  • .sub-navposition: absolute; bottom: 5%; left: 50%; transform: translateX(-50%); display: flex; gap: 0.5em; opacity: 0;. .sub-nav p { font-family: "Canopee"; font-size: 20px; color: #cdc6be; }.

GSAP effect (exhaustive)

State: a single boolean isOpen = false and one paused timeline built once on load.

Setup (runs immediately on DOMContentLoaded)
  1. Grab the underline pseudo-element rule:

const activeItemIndicator = CSSRulePlugin.getRule(".menu-item p#active::after");

  1. const toggleButton = document.querySelector(".burger");
  2. Set the closed/initial state of the giant words: gsap.set(".menu-item p", { y: 225 }); — all three words pushed 225px down, hidden behind each .menu-item's clip-path mask.
  3. const timeline = gsap.timeline({ paused: true });
Timeline (4 tweens, in this exact order and position)

Tween 1 — overlay wipes down. Target .overlay.

  • clipPath: "polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)" (from the CSS initial collapsed-top polygon → full rectangle, i.e. the dark panel unrolls downward covering the viewport).
  • duration: 1.5, ease: "power4.inOut". Inserted at the timeline start (t = 0).

Tween 2 — menu words rise, staggered. Target .menu-item p.

  • y: 0 (from the preset y: 225 → 0, so each word slides up into its mask).
  • duration: 1.5, stagger: 0.2 (Index, then Work, then About, 0.2s apart), ease: "power4.out".
  • Position parameter "-=1" — starts 1s before the previous tween ended, i.e. at t ≈ 0.5s, overlapping the overlay wipe.

Tween 3 — active underline grows. Target activeItemIndicator (the CSSRulePlugin rule).

  • width: "100%" (from 0%100%, the burnt-orange bar draws left→right across the "Index" word).
  • duration: 1, ease: "power4.out", delay: 0.5.
  • Position parameter "<" — inserted at the *start* of Tween 2 (t ≈ 0.5s); with its own delay: 0.5 it effectively begins around t ≈ 1.0s and finishes at ≈ 2.0s.

Tween 4 — social sub-nav fades up. Target .sub-nav.

  • bottom: "10%" (from 5%10%, rises slightly) and opacity: 1 (from 01).
  • duration: 0.5, delay: 0.5.
  • Position parameter "<" — inserted at the start of Tween 3 (t ≈ 0.5s); with delay: 0.5 it effectively begins around t ≈ 1.0s.
Trigger

Add a click listener on .burger:

toggleButton.addEventListener("click", () => {
  if (isOpen) timeline.reverse();
  else timeline.play();
  isOpen = !isOpen;
});

So the first click plays the timeline forward (menu opens), the next click reverses it (everything retracts along the same eases), and so on. The inline onclick on the button independently toggles the .active class that morphs the burger into an X (250ms CSS transition), in sync with the toggle.

Assets / images

One image: hero.jpg — a large, moody, editorial photograph (portrait mood works well) filling the bordered hero frame full-bleed via object-fit: cover. It sits on the base page behind the overlay and is only visible when the menu is closed. Any single high-quality editorial/fashion-style photo works; aspect ratio is flexible since it is cropped to fill the frame.

Behavior notes

  • Desktop-oriented; the 15vw menu type and centered flex layout scale down gracefully. @media (max-width: 900px) only shrinks the logo to 20px.
  • The whole effect is a single reusable paused timeline — no re-creation per click, just play() / reverse().
  • No infinite loops, no scroll hijacking; lightweight and mobile-safe.
  • will-change: transform hints are set on the overlay, menu words and burger bars for smoothness.