Menu
A dropdown menu: a trigger opens a floating list of actions — with groups, checkbox and radio items, nested submenus, and shortcut hints. Decoupled from any particular trigger — pass your own, commonly a Button rendered through the trigger.
Basic
A Menu.Trigger opens the Menu.Content. Compose the trigger from a Button via render so it inherits the button's look, hover, and focus; the content holds the Items.
"use client";
import { Button, Menu } from "@stridge/noctis";
import { ChevronDown } from "lucide-react";
export default function MenuBasic() {
return (
<Menu.Root>
<Menu.Trigger
render={
<Button variant="secondary" endIcon={ChevronDown}>
Options
</Button>
}
/>
<Menu.Content>
<Menu.Item>Edit</Menu.Item>
<Menu.Item>Duplicate</Menu.Item>
<Menu.Item>Archive</Menu.Item>
</Menu.Content>
</Menu.Root>
);
}
With icons
Give items a leading icon, and divide groups with a Separator. Items support disabled like any Base UI menu item, and the menu is fully keyboard-operable — arrow keys, Home/End, and typeahead.
"use client";
import { Button, Menu } from "@stridge/noctis";
import { ChevronDown, Copy, FileText, Link2, Trash2 } from "lucide-react";
export default function MenuWithIcons() {
return (
<Menu.Root>
<Menu.Trigger
render={
<Button variant="secondary" endIcon={ChevronDown}>
Actions
</Button>
}
/>
<Menu.Content>
<Menu.Item icon={Copy}>Copy page</Menu.Item>
<Menu.Item icon={Link2}>Copy link</Menu.Item>
<Menu.Item icon={FileText}>View as Markdown</Menu.Item>
<Menu.Separator />
<Menu.Item icon={Trash2} disabled>
Delete
</Menu.Item>
</Menu.Content>
</Menu.Root>
);
}
Groups
Wrap related items in a Menu.Group with a Menu.GroupLabel — the label is muted, non-interactive, and announced as the group's name. A destructive action takes tone="danger": a red label whose highlight tints toward danger instead of the neutral fill.
"use client";
import { Button, Menu } from "@stridge/noctis";
import { ChevronDown, Copy, Link2, Trash2 } from "lucide-react";
export default function MenuGroups() {
return (
<Menu.Root>
<Menu.Trigger
render={
<Button variant="secondary" endIcon={ChevronDown}>
Page
</Button>
}
/>
<Menu.Content>
<Menu.Group>
<Menu.GroupLabel>Share</Menu.GroupLabel>
<Menu.Item icon={Copy}>Copy page</Menu.Item>
<Menu.Item icon={Link2}>Copy link</Menu.Item>
</Menu.Group>
<Menu.Separator />
<Menu.Group>
<Menu.GroupLabel>Danger zone</Menu.GroupLabel>
<Menu.Item icon={Trash2} tone="danger">
Delete page
</Menu.Item>
</Menu.Group>
</Menu.Content>
</Menu.Root>
);
}
Checkbox & radio items
Menu.CheckboxItem toggles a setting; Menu.RadioGroup with Menu.RadioItems selects exactly one value. Both keep the menu open when activated, so several settings can change in one visit, and both reserve the leading column for their indicator — labels stay aligned with iconed items. Use inset on plain Items sharing a menu with them.
"use client";
import { Button, Menu } from "@stridge/noctis";
import { ChevronDown } from "lucide-react";
import { useState } from "react";
export default function MenuCheckboxRadio() {
const [showToolbar, setShowToolbar] = useState(true);
const [showMinimap, setShowMinimap] = useState(false);
const [sortBy, setSortBy] = useState("name");
return (
<Menu.Root>
<Menu.Trigger
render={
<Button variant="secondary" endIcon={ChevronDown}>
View
</Button>
}
/>
<Menu.Content>
<Menu.CheckboxItem checked={showToolbar} onCheckedChange={setShowToolbar}>
Show toolbar
</Menu.CheckboxItem>
<Menu.CheckboxItem checked={showMinimap} onCheckedChange={setShowMinimap}>
Show minimap
</Menu.CheckboxItem>
<Menu.Separator />
<Menu.Group>
<Menu.GroupLabel>Sort by</Menu.GroupLabel>
<Menu.RadioGroup value={sortBy} onValueChange={setSortBy}>
<Menu.RadioItem value="name">Name</Menu.RadioItem>
<Menu.RadioItem value="modified">Last modified</Menu.RadioItem>
<Menu.RadioItem value="size">Size</Menu.RadioItem>
</Menu.RadioGroup>
</Menu.Group>
</Menu.Content>
</Menu.Root>
);
}
Submenus
Nest a Menu.SubmenuRoot wrapping a Menu.SubmenuTrigger and its own Menu.Content. The submenu opens beside its trigger on the inline-end side, flipping to inline-start at the viewport edge, and the trailing chevron mirrors under RTL. It opens on hover after a short intent delay — with a grace period on leave, so diagonal pointer travel into the submenu doesn't snap it shut — and from the keyboard with Right arrow or Enter. Trailing secondary text (here, the resolved times) flows after the label.
"use client";
import { Button, Menu } from "@stridge/noctis";
import { Bell, ChevronDown, FileDown, FileText, Image } from "lucide-react";
export default function MenuSubmenu() {
return (
<Menu.Root>
<Menu.Trigger
render={
<Button variant="secondary" endIcon={ChevronDown}>
Project
</Button>
}
/>
<Menu.Content>
<Menu.Item icon={FileText}>Edit project</Menu.Item>
<Menu.SubmenuRoot>
<Menu.SubmenuTrigger icon={FileDown}>Export as</Menu.SubmenuTrigger>
<Menu.Content>
<Menu.Item icon={FileText}>PDF document</Menu.Item>
<Menu.Item icon={Image}>PNG image</Menu.Item>
<Menu.Item icon={FileText}>Markdown</Menu.Item>
</Menu.Content>
</Menu.SubmenuRoot>
<Menu.SubmenuRoot>
<Menu.SubmenuTrigger icon={Bell}>Remind me</Menu.SubmenuTrigger>
<Menu.Content>
<Menu.Item>
Next hour
<span className="ms-auto ps-4 text-mini text-subtle">10:30 AM</span>
</Menu.Item>
<Menu.Item>
Tomorrow
<span className="ms-auto ps-4 text-mini text-subtle">9:00 AM</span>
</Menu.Item>
<Menu.Item>
Next week
<span className="ms-auto ps-4 text-mini text-subtle">Mon, 9:00 AM</span>
</Menu.Item>
</Menu.Content>
</Menu.SubmenuRoot>
</Menu.Content>
</Menu.Root>
);
}
Shortcuts
Menu.Shortcut renders a trailing keyboard hint through the Kbd primitive — pass keys in Kbd syntax (Mod resolves to ⌘ or Ctrl by platform). The hint is decorative: it is hidden from assistive tech so the item's accessible name stays clean. When the binding is actually wired up, set aria-keyshortcuts on the item to expose it to screen readers — with the platform's real modifier (Meta+C on macOS, Control+C elsewhere), since the attribute takes literal keys, not the Mod abstraction.
"use client";
import { Button, Menu } from "@stridge/noctis";
import { ChevronDown, Copy, Scissors, Search, Trash2 } from "lucide-react";
export default function MenuShortcuts() {
return (
<Menu.Root>
<Menu.Trigger
render={
<Button variant="secondary" endIcon={ChevronDown}>
Edit
</Button>
}
/>
<Menu.Content>
<Menu.Item icon={Scissors}>
Cut
<Menu.Shortcut keys="Mod+X" />
</Menu.Item>
<Menu.Item icon={Copy}>
Copy
<Menu.Shortcut keys="Mod+C" />
</Menu.Item>
<Menu.Item icon={Search}>
Find
<Menu.Shortcut keys="Mod+F" />
</Menu.Item>
<Menu.Separator />
<Menu.Item icon={Trash2} tone="danger">
Delete
<Menu.Shortcut keys="Backspace" />
</Menu.Item>
</Menu.Content>
</Menu.Root>
);
}
Positioning
The menu opens below the trigger, aligned to its start, by default. Steer it with side and align (and sideOffset / alignOffset) on Menu.Content — alignment is direction-aware, so start/end flip under RTL. collisionPadding (default 8) keeps the menu off the viewport edges, and a long menu caps at the available height and scrolls its list. Noctis menus sit flush against their anchor — there is no arrow/caret part (Base UI offers one; this system doesn't use it).
"use client";
import { Button, Menu } from "@stridge/noctis";
import { ChevronDown } from "lucide-react";
export default function MenuPositioning() {
return (
<div className="flex flex-wrap items-center gap-3">
<Menu.Root>
<Menu.Trigger
render={
<Button variant="secondary" endIcon={ChevronDown}>
Align start
</Button>
}
/>
<Menu.Content align="start">
<Menu.Item>Profile</Menu.Item>
<Menu.Item>Settings</Menu.Item>
</Menu.Content>
</Menu.Root>
<Menu.Root>
<Menu.Trigger
render={
<Button variant="secondary" endIcon={ChevronDown}>
Open upward
</Button>
}
/>
<Menu.Content side="top" align="end">
<Menu.Item>Profile</Menu.Item>
<Menu.Item>Settings</Menu.Item>
</Menu.Content>
</Menu.Root>
</div>
);
}
Keyboard
| Key | Action |
|---|---|
| Enter / Space / ↓ | On the trigger: open the menu and highlight the first item (↑ opens to the last). |
| ↓ / ↑ | Move the highlight to the next / previous item. |
| Home / End | First / last item. |
| → | Open the highlighted submenu and move into it (← under RTL). |
| ← | Close the current submenu and return to its trigger (→ under RTL). |
| Enter / Space | Activate the highlighted item — run an Item, toggle a CheckboxItem, select a RadioItem, open a submenu. |
| Esc | Close the current menu level; on the root, return focus to the trigger. |
| Tab | Close the whole menu and move focus on. |
| Characters | Typeahead — jump to the item whose label matches what you type. |
Disabled items stay reachable by the keyboard and are announced as disabled — they just can't be activated. That follows the ARIA menu pattern: removing them from navigation would hide why an action is unavailable from screen-reader users. Right-click context menus are out of scope for this primitive.
Anatomy
Compose a menu from its parts. Menu.Root owns the open state (it accepts every Base UI Menu.Root prop — open, defaultOpen, onOpenChange, modal).
Menu.Root— owns the open state; renders no element of its own. Modal by default (page scroll locked, outside content inert); passmodal={false}for a lighter popup.Menu.Trigger— opens the menu. Style it directly or compose aButtonthroughrender.Menu.Content— the floating, elevated, animated surface holding the items. Props:side(defaultbottom;inline-endinside a submenu),align(defaultstart),sideOffset,alignOffset,collisionPadding.Menu.Group+Menu.GroupLabel— a labelled section of related items.Menu.Item— a menu action with an optional leadingicon, aninsetalignment helper, and atone(default|danger).Menu.LinkItem— a navigable row rendering an<a>(href,target,rel), with the sameicon/inset/tonerecipe asItem; closes the menu on click by default.Menu.CheckboxItem— a toggling item with an animated check indicator.Menu.RadioGroup+Menu.RadioItem— a single-select set with a dot indicator.Menu.SubmenuRoot+Menu.SubmenuTrigger— a nested submenu; the trigger is an item-shaped row with a trailing, RTL-mirrored chevron.Menu.Shortcut— a trailing, decorative keyboard hint composingKbd.Menu.Separator— a hairline between groups of items.
Every rendered part carries a data-slot (menu-trigger, menu-content, menu-viewport, menu-group, menu-group-label, menu-item, menu-link-item, menu-checkbox-item, menu-radio-group, menu-radio-item, menu-item-indicator, menu-submenu-trigger, menu-shortcut, menu-separator) for host-side styling — pair it with the Base UI state attributes (data-popup-open, data-highlighted, data-disabled, data-checked).
Design tokens
Generated from the component's declaration — the same graph that mints the CSS, so a variable name or its resolution default can't drift. The minted tokens are the public override seam: set one on any ancestor and every menu in that region retunes — e.g. .marketing { --noctis-menu-content-min-width: 18rem; } widens every popup opened beneath it. Knobs that aren't minted are reached through the part's data-slot. See Customization for the full override ladder and Tokens for the whole graph.
API reference
Generated from the component's types — every prop, type, default, and description comes straight from the source. Each part gets its own table; parts that only forward to Base UI list just the props they pass through.
Menu.Root
No props of its own — forwards to the underlying Base UI part.
Menu.Trigger
Menu.Content
Menu.Group
Menu.GroupLabel
Menu.Item
Menu.LinkItem
Menu.CheckboxItem
Menu.RadioGroup
Menu.RadioItem
Menu.SubmenuRoot
No props of its own — forwards to the underlying Base UI part.