# Curtain Menu Overlay

> A fullscreen nav overlay that arrives as layered clip-path curtains, oversized type masking up on a stagger, and a preview plate that wipes in the direction you moved.

Canonical: https://gsapvault.com/effects/curtain-menu-overlay
Live demo: https://gsapvault.com/demos/curtain-menu-overlay/index.html

| Property | Value |
|----------|-------|
| Type | effect |
| Tier | paid |
| Price | £5 |
| Difficulty | advanced |
| Plugins | Core GSAP only |
| Techniques | clip-path, mask-reveal, stagger, hover-effect, click-toggle, keyboard-navigation, velocity-reactive |
| Uses Lenis | No |

## Overview

The award-site fullscreen menu, built as a real component rather than a showreel. Three curtain panels sweep across the page on offset durations and eases, so the overlay arrives with depth instead of landing as one flat colour, and the oversized link type masks up behind them on a stagger that is choreographed against the sweep rather than queued after it.

Moving through the list drives four things at once: the hovered link slides out and takes the accent, the rest dim and recede, an anchored preview plate wipes to that link's image in the direction you travelled, and a colour bloom behind the type re-tints and drifts to the row. Sweep the list fast and the transitions compress and the plate leans into the movement, because each new preview finishes the one in flight instead of joining a queue.

Closing is a genuine reverse: the type retreats from the bottom of the list up, the preview folds away, and only then do the curtains lift, deepest layer first, so the accent is the last thing off the screen.

## Features

- Three-layer clip-path curtain with per-layer duration, ease and offset, so the overlay arrives with depth rather than as one flat panel
- Oversized link type revealed through real overflow masks on a stagger keyed to the curtain, using labelled GSAP timelines you can retime in one place
- Anchored preview plate that wipes between links from the direction the pointer travelled, with a velocity-scaled lean and elastic settle
- Interruption-safe transitions: a wipe caught mid-flight is completed, not abandoned, so a fast sweep down the list never queues a backlog
- One input, four responses: active link shifts and takes the link's tint, siblings dim and recede, the preview wipes, and a background bloom re-tints and drifts to the row
- Magnetic close control with a lagging glyph and an elastic release
- Real dialog behaviour: focus trap, Escape to close, aria-expanded and aria-hidden, inert while closed, and focus returned to the trigger
- Body scroll lock that also stops and restarts Lenis, which ignores overflow: hidden
- Keyboard focus drives the same preview channel as hover, and press-and-slide does on touch
- Progressive enhancement: without JavaScript the whole menu renders as an ordinary nav list, so no link is ever trapped behind a class the script never added

## Use Cases

- Agency, studio and portfolio sites where the menu is part of the brand rather than a utility
- Fashion, architecture and photography sites that want a per-section image preview in the nav
- Product and campaign microsites needing one memorable transition between a handful of pages
- Editorial and magazine sites where section names carry display type worth showing at full size
- Any mobile-first site that wants a fullscreen nav designed at 390px rather than shrunk into it

## Vibe-Code Ready Setup

This effect includes `START-HERE-AI.md`, a product-specific copy-paste setup prompt for Cursor, Claude Code, ChatGPT, GitHub Copilot, Windsurf, and other coding assistants. It tells the assistant to inspect the existing stack, integrate the supplied files, preserve the design, scope selectors, retain accessibility and responsive behaviour, add framework-appropriate GSAP cleanup, and report what it tested.

[How AI-assisted setup works](https://gsapvault.com/vibe-coding)

## How It Works

The curtains are three absolutely positioned panels animated from inset(0% 0% 100% 0%) to inset(0% 0% 0% 0%) at staggered times with different durations and eases. Writing both ends as full inset() rectangles is what keeps GSAP interpolating geometry instead of falling back to a stylesheet's clip-path: none. The link labels are their own overflow masks, with the moving child created by the script, so a buyer's markup stays one span per link; the reveal tweens yPercent and y together, which is what stops a masked line settling permanently offset.

The preview channel is deliberately not an image trail. Where an image-trail effect (effect 021) spawns a pool of cards that follow the cursor and decay, this is a single plate anchored in the layout with two stacked images that swap roles. Each change writes the new source to whichever image is underneath, clips it to the edge the pointer came from, and wipes it open while the outgoing one drifts the other way. If a change arrives mid-wipe the in-flight image is snapped to complete first, so the layer order is always known and a rapid sweep produces one clean transition per link rather than a stack of half-drawn frames.

Switch speed is measured between requests and normalised into a rush value that shortens the wipe and increases the frame's velocity lean, which then springs back on an elastic ease. The same setActive call drives the link stack, the caption and the colour bloom, so hover, keyboard focus and a finger sliding down the list all produce identical results.

Everything runs inside gsap.context() and gsap.matchMedia(), with listeners in a Map for teardown. Under prefers-reduced-motion the overlay still opens, but as a plain cross-fade with every start state restored in CSS, and the magnetic close is never wired up.

## Integration Preview

How this effect integrates into a page. The full documentation (examples, events, programmatic API, customization guide) ships with the download.

### Quick Start

**1. Add to your HTML `<head>`:**

```html
<link rel="stylesheet" href="path/to/style.css">
```

**2. Add before closing `</body>` tag:**

```html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="path/to/script.js"></script>
```

**3. Add a trigger anywhere in your `<body>`:**

```html
<a href="#site-menu" data-curtain-toggle="site-menu"
   aria-controls="site-menu" aria-expanded="false">Menu</a>
```

**4. Add the menu itself, once per page:**

```html
<div class="curtain-menu" id="site-menu" data-curtain-menu>
  <div class="curtain-overlay" data-curtain-overlay role="dialog" aria-modal="true" aria-label="Site menu">
    <div class="curtain-layer curtain-layer--a" data-curtain-layer aria-hidden="true"></div>
    <div class="curtain-layer curtain-layer--b" data-curtain-layer aria-hidden="true"></div>
    <div class="curtain-layer curtain-layer--c" data-curtain-layer aria-hidden="true"></div>
    <div class="curtain-bloom" data-curtain-bloom aria-hidden="true"></div>

    <div class="curtain-inner">
      <div class="curtain-head" data-curtain-chrome>
        <span class="curtain-mark">Your Brand</span>
        <a class="curtain-close" href="#top" data-curtain-close data-curtain-magnetic aria-label="Close menu">
          <span class="curtain-close__glyph" aria-hidden="true"><i></i><i></i></span>
        </a>
      </div>

      <nav class="curtain-body" data-curtain-list aria-label="Main">
        <ul class="curtain-list">
          <li class="curtain-item">
            <a class="curtain-link" href="/work"
               data-curtain-media="/img/work.webp"
               data-curtain-tint="#e8452b"
               data-curtain-meta="24 built projects">
              <span class="curtain-link__index" data-curtain-index aria-hidden="true">01</span>
              <span class="curtain-link__label" data-curtain-label>Work</span>
            </a>
          </li>
          <!-- ...one <li> per link -->
        </ul>
      </nav>

      <aside class="curtain-preview">
        <div class="curtain-preview__frame" data-curtain-frame>
          <img class="curtain-preview__plate" data-curtain-plate src="/img/work.webp" alt="">
        </div>
        <p class="curtain-preview__caption" data-curtain-caption>24 built projects</p>
      </aside>

      <div class="curtain-foot" data-curtain-chrome>
        <span>Left footer slot</span><span>Right footer slot</span>
      </div>
    </div>
  </div>
</div>
```

That is the whole integration. The script finds every `[data-curtain-menu]`, wraps each label's text in its own moving element, clones the preview plate into a second layer, preloads the images, and wires pointer, keyboard, focus and touch handling.

### Options

Root options, on the `[data-curtain-menu]` element:

| Attribute | Values | Default | Description |
|-----------|--------|---------|-------------|
| `data-curtain-menu` | marker | required | Makes the element a curtain menu |
| `data-curtain-autoplay` | milliseconds | off | Opens the menu by itself this long after load. Useful for a landing page that leads with the menu; leave it off for a normal site |
| `data-curtain-speed` | number | `1` | Multiplies every duration in the open and close choreography. Above 1 is slower |
| `data-curtain-stagger` | seconds | `0.075` | Gap between curtain layers, and between link reveals |
| `data-curtain-shift` | pixels | `34` | How far the active label slides sideways. Raise the label's right padding to match |
| `data-curtain-magnet` | pixels | `150` | Radius of the close control's magnetic field. Fine pointers only |
| `data-curtain-scroll-lock` | `true`, `false` | `true` | Set `false` if your own dialog manager already locks scrolling |

Per-link options, on each `[data-curtain-media]` anchor:

| Attribute | Values | Default | Description |
|-----------|--------|---------|-------------|
| `data-curtain-media` | URL | required | The preview image for this link. Also marks the element as a menu link |
| `data-curtain-tint` | any CSS colour | white | Drives the active label's colour, the bloom, the index numbers and the focus ring |
| `data-curtain-meta` | text | none | Written into `[data-curtain-caption]` when this link becomes active |

Trigger options, on any `[data-curtain-toggle]` element anywhere on the page:

| Attribute | Values | Default | Description |
|-----------|--------|---------|-------------|
| `data-curtain-toggle` | menu `id`, or empty | empty | Empty matches every menu on the page. Set it to a menu's `id` when there is more than one |

### Accessibility

- **Reduced motion**: with `prefers-reduced-motion: reduce` the overlay still opens and closes, but as a plain cross-fade. Every animated start state is restored in CSS, the preview swaps instantly instead of wiping, and the magnetic close is never wired up
- **No JavaScript**: the overlay drops out of fixed positioning and renders as an ordinary nav block at the end of the page, with the trigger acting as an in-page link to it. Nothing is hidden behind a class the script never added
- **Focus**: opening from a click or keypress moves focus into the overlay and traps Tab and Shift+Tab inside it; closing returns focus to the element that opened it. An autoplay open does not steal focus, but the trap engages the moment you Tab
- **Escape** closes the menu from anywhere
- **State**: the overlay is `inert` and `aria-hidden="true"` while closed, and every trigger's `aria-expanded` tracks it
- **Keyboard drives the preview**: focusing a link runs exactly the same channel as hovering it, so the image, caption, tint and bloom all follow keyboard navigation
- **Touch**: tapping a link previews it, and pressing and sliding down the list wipes through the previews the way a pointer sweep does. The list keeps `touch-action: pan-y` so the page still scrolls
- **Scroll lock**: the body is locked while the menu is open, and Lenis is stopped and restarted with it (Lenis runs its own loop and ignores `overflow: hidden`)
- **Labels**: give the close control an `aria-label` and the `<nav>` an `aria-label`; the dialog wrapper wants `role="dialog"`, `aria-modal="true"` and its own label

### Dependencies

**Required:**

- GSAP 3.12+ (core only, no plugins)

**Optional:**

- Lenis. The effect does not need it, but if `window.lenis` exists the scroll lock stops and restarts it.

## What You Get

- `index.html`: working demo page
- `assets/script.js`: commented, readable source
- `assets/style.css`: effect styles
- `README.md`: full documentation with examples and framework integration notes
- `START-HERE-AI.md`: product-specific copy-paste prompt for AI-assisted setup
- `LICENSE.txt`: standard license terms
- Lifetime updates: re-download anytime from your library

## Get the Code

This is a premium effect. The standard license costs £5 one-time and covers unlimited personal and commercial projects with no attribution required. The only restrictions: no redistribution of the code itself and no competing effect libraries.

- [Buy Curtain Menu Overlay](https://gsapvault.com/effects/curtain-menu-overlay)
- [Effects & Templates Vault (£39 one-time, best value): every current and future effect and template](https://gsapvault.com/effects)

## Judge the Code Quality First

These related effects are free with complete source published, written to the same production standard (cleanup functions, reduced-motion support, framework-agnostic):

- [Hover Underline](https://gsapvault.com/effects/hover-underline.md): Three material link underlines—an exit-through line, marker sweep, and hand-drawn wave—with coordinated type and active-index responses.

---

From [GSAP Vault](https://gsapvault.com): production-ready GSAP animation effects. Full catalog for agents: https://gsapvault.com/llms-full.txt
