045

Curtain Menu Overlay

£5

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.

advanced
7 more details
clip-pathmask-revealstaggerhover-effectclick-togglekeyboard-navigationvelocity-reactive
Curtain Menu Overlay - GSAP animation effect preview

This demo reads better at your own screen size than in the frame below:

About this effect

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.

Read the full effect overview

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 soft bloom of the accent behind the type 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.

What's included

10 items
  • 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

Perfect for

5 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

How it works

4 sections

The curtains are three panels wiped open from fully closed at staggered times with different durations and eases. Both ends are written as complete rectangles, which is what keeps GSAP interpolating the geometry instead of falling back to the stylesheet. The link labels are their own overflow masks, with the moving child created by the script, so your markup stays one span per link, and the reveal is handled so a masked line can never settle 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 with a single setup and teardown path and one motion branch, with listeners kept together for cleanup. 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.

Difficulty Advanced
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

Google Lighthouse on this effect's demo, 15 September 2026. A measurement of the demo as shipped, not a promise for your page.

Accessibility
100
Best practices
100

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

Paid effect

Purchase to unlock the code.

Buying Curtain Menu Overlay opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects.

£5 Standard license, unlimited projects

Browse free effects

Documentation

Quick Start

1. Add to your HTML <head>:

Code snippet omitted: it ships with the download.

2. Add before closing </body> tag:

Code snippet omitted: it ships with the download.

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

Code snippet omitted: it ships with the download.

4. Add the menu itself, once per page:

Code snippet omitted: it ships with the download.

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.

Using It With Your Own Design

What the effect needs from your markup. The hooks above are the API, and the script queries nothing else. The required ones are [data-curtain-menu] (the root, with an id if you want a targeted trigger), [data-curtain-overlay] inside it, at least one [data-curtain-media] link, and [data-curtain-frame] containing one [data-curtain-plate] image. Everything else is optional and degrades quietly: drop [data-curtain-bloom] and the background stops responding; drop [data-curtain-caption] and no caption is written; drop [data-curtain-magnetic] and the close control is an ordinary button; use one [data-curtain-layer] instead of three and you get a flat sweep instead of a layered one.

Give each link a [data-curtain-label] span holding just the label text. The script moves that text into a generated child, so do not depend on the text being a direct child of the span after load, and do not put the index number inside it (that is what [data-curtain-index] is for). Everything that should fade in with the frame carries data-curtain-chrome; add it to any extra header, footer or aside you introduce.

What is only the demo's styling. Everything under the "DEMO: THE PAGE BEHIND" banner in style.css (.page, .page__bar, .page__lede, .page__facts, .page__plate, .page__cue, .page__menu) is the studio page the curtain covers. Delete all of it, along with the <main class="page"> block in index.html, and put your own site there. The tokens at the top of style.css are yours to change: the page's --page / --ink set, the overlay's --curtain-ground, --curtain-layer-a/b/c, --curtain-ink and --curtain-plate, and the single --accent. The script reads three of them at init (--curtain-ink for the resting label colour, --accent as the tint a link falls back to, --curtain-bloom-opacity for how strong the bloom gets), so a reskin is a stylesheet edit and nothing else. Per-link data-curtain-tint values override the accent for that link; the demo sets every link to the one accent, but each can carry its own colour.

The demo loads Mona Sans (weights 400 to 800) from Google Fonts; the oversized labels are set at weight 800. Swap the --font-sans stack for your own face and re-check the label's padding-right against data-curtain-shift, since a wider face needs more slack in the mask.

CSS the effect depends on. Keep the "CURTAIN MENU - EFFECT CONTRACT" section. Specifically:

  • .curtain-overlay must be position: fixed; inset: 0; overflow: hidden. The overflow is what stops a mid-sweep curtain painting outside the viewport.
  • The .has-js start states (clip-path: inset(0% 0% 100% 0%) on the layers and the preview frame, translateY(118%) on .curtain-link__reveal, opacity: 0 on the indices and chrome) must stay gated on .has-js and must stay restored inside the prefers-reduced-motion block. Ungate them and the menu is invisible without JavaScript; remove them and the reveal paints visible before it animates.
  • .curtain-link__label needs overflow: hidden (it is the mask) and a right padding at least as large as data-curtain-shift, or the mask clips the last letter off the active link.
  • .curtain-link__reveal must stay display: block.
  • .curtain-preview__frame needs overflow: hidden and position: relative; the plates are position: absolute; inset: 0 inside it.
  • .curtain-body keeps touch-action: pan-y so a phone can still scroll while a finger slides across the rows.
  • html.curtain-locked is the scroll lock the script toggles.
  • The no-JavaScript block (html:not(.has-js) ...) is what turns the overlay into a static nav list. If you delete it, the menu disappears entirely when the CDN fails.

Where the images come from. data-curtain-media is just a URL, so previews can be photographs, posters, renders or generated SVG data URIs. Give the plate a real alt="" (decorative, since the link text already names the destination) and size the frame in CSS; the images are object-fit: cover.

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 the stylesheet's --accent Drives the active label's colour, the bloom, the close control's hover fill 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

Performance Notes

  • Two <img> elements total, whatever the size of the menu. There is no pool and no element churn: the same two plates swap roles for every transition
  • Preview images are preloaded at init, so the first pass through the list never shows an empty frame
  • A transition interrupted mid-flight is completed rather than abandoned, so a fast sweep runs one wipe per link instead of accumulating half-drawn layers
  • The only continuously animated properties are clip-path and transforms; the bloom is a single blurred element, not a filter over the composition
  • Nothing runs while the menu is closed: no ticker, no ScrollTrigger, no listeners on the page behind beyond the trigger's click

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.

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.

Your cart

Your cart is empty

The Vault £99

The Vault library, plus future additions to the library.