# Menu Notch

> A pill with a menu toggle and a theme button that unfolds into a panel of grouped links; its icon crosses into an X, its label blurs in letter by letter and every link rolls to its hover color.

- Page: https://ui.devigner.cc/components/menu-notch
- Category: Marketing
- Requirements: React 18 or 19, Tailwind CSS v4
- Install: `npx devignerui add menu-notch` (writes components/ui/menu-notch.tsx)
- Packages it needs: cn, motion, @devigner-ui/icons, devignerui

## What the demo shows

The live demo is a site menu: a pill with Menu and a sun button. Open it and the panel unfolds under the pill while the two lines cross into an X and Close blurs in letter by letter; run the pointer down the links and each one rolls up to a tinted copy, slow off the mark and then gliding in. The sun button switches the page between light and dark, its sun morphing into a moon.

## When to use it

- A compact site or app menu that sits at the top of the page and opens over the content, with a handful of main links and a few secondary ones.
- Landing pages and portfolios where the menu itself should feel crafted: the theme button comes with it.

## When to reach for something else

- Deep navigation with nested levels or dozens of links. Use a sidebar or a full menu page.
- Actions on a selection, such as Rename or Delete. Use Context Menu or Command Palette.

## Usage

```tsx
import { MenuNotch } from "@/components/ui/menu-notch";

<MenuNotch sections={sections} socials={socials} />
```

Custom composition:

```tsx
<MenuNotch sections={sections} theme={theme} onThemeChange={setTheme} className="w-120" />
```

### Site menu wired to a theme

Pass your theme state so the round button shows the right icon and switches it.

```tsx
import { useState } from "react";
import { MenuNotch, type MenuNotchSection, type MenuNotchTheme } from "@/components/ui/menu-notch";

const sections: MenuNotchSection[] = [
  {
    label: "Menu",
    size: "lg",
    links: [
      { label: "About", href: "/about" },
      { label: "Pricing", href: "/pricing" },
      { label: "Resources", href: "/resources" },
    ],
  },
  { label: "Legal", links: [{ label: "Privacy Policy", href: "/privacy" }, { label: "Terms & Conditions", href: "/terms" }] },
];

export function Header() {
  const [theme, setTheme] = useState<MenuNotchTheme>("light");
  return (
    <header className={theme === "dark" ? "dark" : undefined}>
      <MenuNotch sections={sections} theme={theme} onThemeChange={setTheme} />
    </header>
  );
}
```

### Close the menu after navigating

Control open so a client-side navigation also folds the panel away.

```tsx
import { useState } from "react";
import { MenuNotch } from "@/components/ui/menu-notch";

export function AppMenu({ go }: { go: (path: string) => void }) {
  const [open, setOpen] = useState(false);
  const link = (label: string, path: string) => ({
    label,
    onSelect: () => {
      go(path);
      setOpen(false);
    },
  });
  return (
    <MenuNotch
      open={open}
      onOpenChange={setOpen}
      sections={[{ label: "Menu", size: "lg", links: [link("Inbox", "/inbox"), link("Settings", "/settings")] }]}
    />
  );
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| sections | `MenuNotchSection[]` | - | Groups of links, in order: { label, links, size? }. Each link is { label, href?, onSelect? }: a link with href, else a button. size "lg" shows 22.5px links with a divider below (the main links); "sm" (the default) shows 16.5px links. |
| socials | `MenuNotchSocial[]` | - | Icon links in a row at the bottom, sized 20px: { label, href, icon }. label is read by screen readers. |
| socialsLabel | `string` | Social media | Heading above the social icons. |
| open | `boolean` | - | Controlled open state. Pass with onOpenChange. |
| defaultOpen | `boolean` | false | Open on first render when uncontrolled. |
| onOpenChange | `(open: boolean) => void` | - | Called when the toggle, Escape or a click outside opens or closes the menu. |
| theme | `"light" \| "dark"` | "light" | Theme the round button shows: a sun for light, a moon for dark, morphing between them when it changes. Pass your site's current theme. |
| onThemeChange | `(theme: "light" \| "dark") => void` | - | Shows the sun/moon button and is called with the theme to switch to. Without it the button is hidden. |
| menuLabel | `string` | Menu | Toggle text while the menu is closed. |
| closeLabel | `string` | Close | Toggle text while the menu is open. |
| classNames | `MenuNotchClassNames` | - | Slot classes: bar (the pill), panel (the surface that grows open) and link (every link and social icon). |
| className | `string` | w-103.5 | Classes on the menu. Its width is the panel's width, 414px by default. |

## Keyboard

| Keys | Action |
| --- | --- |
| Tab | Moves through the toggle, the theme button, then every link while the menu is open. |
| Enter / Space | Opens or closes the menu from the toggle, and follows a focused link. |
| Escape | Closes the menu. |

## Accessibility

- The menu is a nav landmark named "Main"; pass aria-label to rename it.
- The toggle is a button with aria-expanded and aria-controls pointing at the panel, and is named by its current label (Menu or Close). The letters themselves are hidden from screen readers.
- While closed the panel is inert and aria-hidden, so its links are out of the tab order.
- Each group is a list labelled by its heading. Social links are named by their label, since they only show an icon.
- A link rolls to its tinted copy on keyboard focus as well as on hover; the copy is aria-hidden.
- Honors prefers-reduced-motion and an ancestor <MotionConfig reducedMotion="always">, so an in-app motion switch works too.

## Theming

- The pill is foreground with background text, so it is dark on a light page and light on a dark one, and the panel is background. Section headings use muted-foreground and the divider is foreground at 8%.
- Links and icons tint to primary on hover and focus, fading over 400ms on ease-tint. Change it through classNames.link, such as hover:text-destructive focus-visible:text-destructive.
- Panel, pill and headings fade over 500ms when the theme changes (the pill on ease-out, the rest on ease-tint), so a theme switch crossfades the whole menu. A theme switch run inside a view transition replaces this with the transition's own animation.
- The panel opens on SPRING_PILL, which also turns the icon; links roll over 600ms on EASE_ROLL, and the toggle's letters sharpen out of a 3px blur on TWEEN_LETTER, 50ms apart.
- className sizes the menu: w-103.5 (414px) by default.

## Edge cases

- The menu takes 78px of height in the layout; the panel opens over whatever is below it, so give it a positioned spot near the top of the page.
- Clicking outside the menu or pressing Escape closes it.
- Without onThemeChange the sun/moon button is not rendered. theme only picks the icon: switching your site's theme is up to onThemeChange.
- Only sections with size "lg" get a divider below them.

## Troubleshooting

**The panel opens behind other content.**
The panel is absolutely positioned with z-50 inside the menu. If a parent creates its own stacking context with a lower z-index, raise that parent, or the menu itself with className="z-50".

**The sun/moon button doesn't show.**
It only renders when onThemeChange is passed. Wire it to your theme state, and pass theme so it shows the right icon.

**The component renders with no background or the wrong colors.**
Colors use the standard shadcn/ui tokens only (background, foreground, primary, secondary, muted, accent, border, input, ring, destructive), so a project set up with shadcn/ui needs nothing extra and the component follows its theme, dark mode included. Without shadcn/ui, define those CSS variables for :root and .dark and map them in your Tailwind v4 @theme, or run npx shadcn init.
