All components

Audemars Piguet Overlay Menu

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

Open live demo ↗ Raw prompt (.md)

What it does

Fullscreen overlay menu opened from a hamburger toggle: two background halves pre-rotated 180deg with scale(2) sweep in like closing doors on a custom cubic-bezier ease while the bars morph into an X, then SplitText-masked menu links and footer lines slide up with staggered power3.out reveals. Clicking the toggle again reverses the same paused timeline to close.

How it's built

Categorymenu
Techgsap
GSAP pluginsSplitText, CustomEase
Complexitysection
Performance costmedium
Mobile-safeyes

overlay menu hamburger split-text custom-ease rotate-reveal masked-lines

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 Overlay Menu — Rotating Background Doors + Masked Line Reveal

Goal

Build a fixed top navbar with a circular hamburger toggle that opens a fullscreen overlay menu. The signature effect: two full-height background halves, each pre-rotated 180° and scaled ×2 around their inner edge, rotate back to 0° so they sweep in like two closing blades/doors on a custom cubic-bezier ease, while the hamburger bars morph into an X at the same time; then two columns of menu links plus a footer reveal line-by-line from a mask with a staggered power3.out slide-up. It is a single paused GSAP timeline: clicking the toggle .play()s it to open, clicking again .reverse()s the exact same timeline to close.

Tech

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

import gsap from "gsap";
import { SplitText } from "gsap/SplitText";
import { CustomEase } from "gsap/CustomEase";
gsap.registerPlugin(SplitText, CustomEase);

Layout / HTML

nav                                  (fixed top bar, 3 equal flex columns)
  .nav-toggle
    .nav-toggle-btn                  (circular 60×60 hamburger button — click target)
      span.bar-1
      span.bar-2
  .nav-logo   > a  "Carbon Structure"     (centered wordmark)
  .nav-cta    > a  "Start Journey"        (right pill button)

.menu                                (fixed fullscreen overlay, pointer-events:none until open)
  .menu-bg
    .menu-bg-left  > .menu-bg-left-inner       (left half, its inner is the animated blade)
    .menu-bg-right > .menu-bg-right-inner       (right half, its inner is the animated blade)
  .menu-items                        (flex row)
    .menu-items-col   (col 1)  → 5 × .menu-link > a   (uppercase sans links)
    .menu-items-col   (col 2)  → 5 × .menu-link > a   (serif links)
    .menu-footer                     (absolute bottom bar, space-between)
      .menu-footer-col  → 4 × a  (legal links)
      .menu-footer-col  → p "© 2025 Carbon Structure"

section.hero > h1  "Dense Geometry"  (fullscreen background image sits behind the menu)

Use neutral, fictional menu labels — no real brand names. Col 1 links (uppercase): Manifesto, Spatial Journeys, Material Archive, Visit Atelier, Rituals. Col 2 links (serif): Tactile Vault, Form Experiments, Carbon Network, Shadow Library, Collections. Footer legal links: Usage Terms, Data & Cookies, Privacy Policy, Accessibility. Wordmark and hero heading are neutral placeholders.

Styling

Fonts (Google Fonts): Instrument Sans (body/UI, variable 400–700, has italics) and Instrument Serif (display/serif accent). body { font-family: "Instrument Sans"; }.

Color tokens:

  • --base-100: #fff (all text is white)
  • --base-200: #474437 (left background blade — muted olive/brown)
  • --base-300: #403d31 (right background blade — slightly darker olive)

Global: * { margin:0; padding:0; box-sizing:border-box; }. a, p { text-decoration:none; color:var(--base-100); font-weight:450; line-height:1; }.

Type:

  • .hero h1: Instrument Serif, font-size: clamp(3rem, 10vw, 12rem), weight 500, line-height: 0.75, width: 50%, centered, color white.
  • .menu-items-col:nth-child(1) a: uppercase (Instrument Sans), font-size: clamp(1.5rem, 2.5vw, 4rem), line-height: 1.1.
  • .menu-items-col:nth-child(2) a: Instrument Serif, font-size: clamp(1.65rem, 2.75vw, 3rem), line-height: 1.2.
  • .menu-footer a, .menu-footer p: uppercase, font-size: 0.75rem.
  • nav .nav-logo a: width: 8rem, centered, uppercase, font-size: 0.9rem, weight 500.
  • nav .nav-cta a: Instrument Serif, padding: 0.75rem 1.25rem, border: 1px solid rgba(255,255,255,0.15), border-radius: 5rem (pill).

Structural / load-bearing CSS:

  • .hero: position: relative; width: 100%; height: 100svh; fullscreen background image no-repeat 50% 50% / cover, flex-centered, overflow: hidden.
  • nav: position: fixed; width: 100%; padding: 2rem; flex, align-items: center, z-index: 2. nav > div { flex: 1; }. .nav-logo centers its child; .nav-cta right-aligns its child.
  • .nav-toggle-btn: width: 60px; height: 60px; padding: 20px; flex column, justify-content: center; gap: 5px; border: 1px solid rgba(255,255,255,0.15); border-radius: 10rem; cursor: pointer (a circular ring). Each span (.bar-1, .bar-2): width: 100%; height: 1.25px; background: var(--base-100); will-change: transform; (two thin white bars ~5px apart inside a 20px-wide content box).
  • .menu: position: fixed; inset: 0; width: 100%; height: 100svh; pointer-events: none; overflow: hidden; z-index: 1;. .menu.active { pointer-events: all; } (JS toggles active).
  • .menu-bg: position: absolute; inset: 0; width:100%; height:100%; pointer-events:none;.
  • .menu-bg-left, .menu-bg-right: position: absolute; width: 50%; height: 100%; overflow: hidden; — left docked left:0, right docked right:0. The overflow:hidden is what clips the oversized rotating inner blade.
  • .menu-bg-left-inner, .menu-bg-right-inner: position: absolute; width: 100%; height: 100%; will-change: transform;.
  • .menu-bg-left-inner: background-color: var(--base-200); transform-origin: 100% 50%; transform: rotate(180deg) scale(2, 2);
  • .menu-bg-right-inner: background-color: var(--base-300); transform-origin: 0% 50%; transform: rotate(-180deg) scale(2, 2);
  • i.e. each half's inner panel is doubled in size and flipped 180° about the inner (center-facing) edge, so at rest it is swung out of frame; animating rotation to 0 swings it back to cover the half.
  • .menu-items: position: absolute; inset: 0; width:100%; height:100%; flex row. .menu-items-col: flex: 1; flex column, justify-content: center; align-items: center; gap: 2rem;.
  • .menu-footer: position: absolute; bottom: 0; left: 0; width: 100%; padding: 2rem; flex space-between, align-items: flex-end. .menu-footer-col { display:flex; gap:2rem; }.
  • Masked-line initial state (set in CSS, matched by SplitText output): .menu a .line, .menu p .line { position: relative; transform: translateY(110%); will-change: transform; } — every split line starts pushed down 110%, hidden behind its line-mask.

GSAP effect (exhaustive)

0. CustomEase

Register one custom ease before building the timeline:

CustomEase.create("hop", "0.85, 0, 0.15, 1");

This "hop" ease (a symmetric slow-in/slow-out, ~cubic-bezier(0.85, 0, 0.15, 1)) drives the background blades and the hamburger morph.

1. SplitText — split every menu text into masked lines

Run once at init, before the timeline:

SplitText.create(".menu a, .menu p", {
  type: "lines",
  mask: "lines",
  linesClass: "line",
});
  • Selector targets all anchors and the copyright <p> inside .menu (both link columns + all footer text).
  • type: "lines" splits into line elements; mask: "lines" wraps each line in an overflow-hidden mask so the line can be hidden below it; linesClass: "line" gives each line the .line class that the CSS pre-offsets to translateY(110%).
2. The single paused timeline
const tl = gsap.timeline({ paused: true });

Add these tweens (positions noted — this is the exact order and timing):

At absolute time 0 (all four fire together, ease: "hop", duration: 1):

  1. .nav-toggle-btn .bar-1{ y: 3.25, rotation: 45, scaleX: 0.75 } — top bar drops 3.25px, rotates +45°, shrinks to 75% width.
  2. .nav-toggle-btn .bar-2{ y: -3.25, rotation: -45, scaleX: 0.75 } — bottom bar rises 3.25px, rotates −45°, shrinks to 75%. Together the two bars form an X.
  3. .menu .menu-bg-left-inner{ rotate: 0 } — swings the left blade from 180° back to 0° (sweeping in from the center edge to cover the left half).
  4. .menu .menu-bg-right-inner{ rotate: 0 } — swings the right blade from −180° back to 0° (covering the right half).

At absolute time 0.6 (position param passed as the string "0.6"):

  1. .menu-items-col:nth-child(1) .line{ y: 0 }, duration: 0.75, ease: "power3.out", stagger: 0.1 — column-1 link lines rise from translateY(110%) up into view, one after another 0.1s apart.

At the same start as the previous tween (position "<", i.e. also 0.6):

  1. .menu-items-col:nth-child(2) .line{ y: 0 }, duration: 0.75, ease: "power3.out", stagger: 0.1.
  2. .menu-footer .line{ y: 0 }, duration: 0.75, ease: "power3.out", stagger: 0.1.

So the two background blades + hamburger X animate over 0→1s on the "hop" ease, and starting at 0.6s the two link columns and the footer all reveal their lines simultaneously (each column staggered internally 0.1s) with power3.out. Total timeline ≈ 1.35s.

3. Toggle wiring
const menu = document.querySelector(".menu");
const menuToggle = document.querySelector(".nav-toggle-btn");
let isMenuOpen = false;

menuToggle.addEventListener("click", () => {
  if (isMenuOpen) {
    tl.reverse();
    menu.classList.remove("active");
  } else {
    tl.play();
    menu.classList.add("active");
  }
  isMenuOpen = !isMenuOpen;
});
  • Open: tl.play() runs the timeline forward and .menu gets active (enables pointer events).
  • Close: tl.reverse() plays the same timeline backwards — lines drop back into their masks, blades swing back out to ±180°, the X un-morphs into a hamburger — and active is removed.
  • No re-entrancy guard; GSAP handles mid-flight reversal naturally.

Assets / images

1 image, role = *fullscreen hero background* sitting behind the whole menu. Square (1:1) source. Subject: a single centered abstract 3D render of a soft, organically-twisted torus/ring (a doughnut-shaped loop with gentle wavy, molten undulations) resting on a flat surface, lit from above. Colors are a cool, near-monochrome palette — a brushed/satin silver-metallic ring in soft light and mid grays against a plain medium-gray studio background, with subtle darker shadow beneath. Clean, minimal, low-contrast and neutral, so the white nav/menu text and the olive blade overlay read clearly on top of it. Displayed background-size: cover at 50% 50%.

Behavior notes

  • Responsive (max-width: 1000px): nav becomes flex-direction: row-reverse; justify-content: space-between; (toggle right, wordmark left); .nav-cta is hidden. The right background half is hidden and .menu-bg-left grows to width: 100% (single blade fills the screen). .menu-items becomes padding: 6rem 2rem; flex-direction: column-reverse; gap: 4rem;, columns switch to align-items: flex-start; gap: 0.5rem, and the footer columns stack (flex-direction: column; gap: 0).
  • Menu overlay is pointer-events: none until opened (.active), so it never blocks the hero when closed.
  • All motion is click-driven off one paused, reversible timeline — no scroll, no autoplay, no loops.