Skip to content
WANCRAFT

D.75Navigation & Menus

Overlay Menu

A fullscreen navigation overlay that grows from its hamburger button, with large links rising through masks.

NewReactMotionCSSTailwindintermediate

Overview

Overlay Menu pairs a hamburger button that morphs into a cross with a fullscreen navigation overlay. The overlay's background grows as a clip-path circle from the centre of the button, then large editorial links rise one after another through overflow masks while a secondary column (contact details, socials, hours) fades in. It is a native <dialog> opened with showModal, so it sits in the top layer with an inert page behind it, traps focus, closes on Escape and returns focus to the button. Body scroll is locked while it is open, the current page's link carries aria-current="page", and the whole sequence plays in reverse on close. With reduced motion the overlay simply fades.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/overlay-menu.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/overlay-menu.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/overlay-menu.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { OverlayMenu, type OverlayMenuItem } from "@/components/ui/overlay-menu";

export function OverlayMenuExample() {
  const items: OverlayMenuItem[] = [
    { label: "Work", href: "/work", meta: "24" },
    { label: "Studio", href: "/studio" },
    { label: "Journal", href: "/journal", meta: "New" },
    { label: "Contact", href: "/contact" },
  ];

  return (
    <header className="flex items-center justify-between p-6">
      <span>Studio</span>
      <OverlayMenu
        items={items}
        currentHref="/work"
        aside={<p>hello@studio.com</p>}
      />
    </header>
  );
}

03Props

PropTypeDefaultDescription
itemsOverlayMenuItem[]—{ label: string; href: string; meta?: string }.
currentHrefstring—href of the current page; that link gets aria-current="page".
openboolean—Open state (controlled).
defaultOpenbooleanfalseInitial open state (uncontrolled).
onOpenChange(open: boolean) => void—Called when the overlay asks to open or close.
onNavigate(item, event) => void—Called when a link is activated; call event.preventDefault() for client routing.
closeOnNavigatebooleantrueClose the overlay after a link is activated.
asideReactNode—Secondary column content, shown from the md breakpoint.
reveal"circle" | "fade""circle"Circle growing from the button, or a fade.
tone"inverse" | "accent" | "surface""inverse"Overlay colours.
showIndexbooleantrueShow 01, 02… before each link.
durationnumber700Background reveal length, in ms.
staggernumber60Delay between links, in ms.
accentstring"#FF7448"Accent tone, current marker and focus ring colour.
titlestring"Menu"Accessible name of the dialog and nav.
openLabelstring"Open menu"Accessible name of the trigger.
closeLabelstring"Close menu"Accessible name of the close button.
classNamestring—Classes added to the trigger button.

04Features

  • 01Hamburger-to-cross morph on both the trigger and the in-overlay close button
  • 02Clip-path circle reveal measured from the trigger's centre to the farthest viewport corner
  • 03Large editorial links rise through overflow masks with a configurable stagger, and retreat in reverse
  • 04Secondary information column that fades in after the links
  • 05Native modal <dialog>: top layer, inert page, focus trap, Escape, focus return
  • 06Body scroll lock with scrollbar compensation; controlled or uncontrolled
motion · tweenmotion · staggerinput · clickinput · keyboardinput · hover

05Accessibility

  • The trigger is a button with aria-haspopup="dialog", aria-expanded and aria-controls pointing at the dialog.
  • The dialog is opened with showModal(): the page is inert, Tab stays inside, Escape closes, and focus returns to the trigger.
  • Links sit in a labelled <nav> list; the current page's link has aria-current="page".
  • Hover dimming only applies on devices that can hover; every link keeps a visible focus ring.
  • With prefers-reduced-motion the overlay and links fade without movement or circle reveal.

06Limitations

  • The secondary column is hidden below the md breakpoint to keep the links large on phones.