A popover list of actions and choices, with key hints, sections and router links.
Basic
A list of actions that opens from a button, in a Popover.
- Items: each has a label, and can have an icon, a second line, a preformatted value at its end and a key hint.
Modin a hint is Cmd on Apple platforms and Ctrl elsewhere. - Sections: group items under a heading. A separator rules between groups.
- Danger:
variant="danger"shows the label and icon in the error color, for an item that loses something.
Rows are 40px tall, and 44px on a touch screen.
Example
Example
Nothing chosen yet.
'use client';
import {
IconButton,
Menu,
MenuItem,
MenuSection,
MenuSeparator,
MenuTrigger,
Text,
} from '@snapshot-ui/react';
import { Copy, Download, Ellipsis, Pencil, Share2, Trash2 } from 'lucide-react';
import { useState, type ReactElement } from 'react';
const LABELS: Record<string, string> = {
rename: 'Rename',
duplicate: 'Duplicate',
share: 'Share',
delete: 'Delete the project',
};
export default function MenuBasic(): ReactElement {
const [chosen, setChosen] = useState<string | null>(null);
return (
<div style={{ display: 'grid', gap: 16, justifyItems: 'center' }}>
<MenuTrigger>
<IconButton aria-label="Project actions" variant="outline">
<Ellipsis />
</IconButton>
<Menu
onAction={(key) => {
setChosen(LABELS[String(key)] ?? null);
}}
>
<MenuSection title="Project">
<MenuItem id="rename" icon={<Pencil />}>
Rename
</MenuItem>
<MenuItem id="duplicate" icon={<Copy />} shortcut="Mod D">
Duplicate
</MenuItem>
<MenuItem id="share" icon={<Share2 />} description="Anyone with the link can view">
Share
</MenuItem>
<MenuItem
id="export"
icon={<Download />}
description="On the Team plan"
shortcut="Mod E"
isDisabled
>
Export
</MenuItem>
</MenuSection>
<MenuSeparator />
<MenuItem id="delete" icon={<Trash2 />} variant="danger">
Delete the project
</MenuItem>
</Menu>
</MenuTrigger>
<Text tone="muted">{chosen === null ? 'Nothing chosen yet.' : `Chose ${chosen}.`}</Text>
</div>
);
}Selection
A menu can also choose. Set selectionMode on the menu, or on a section to make it choose apart from the rest, as the theme and panels do here.
selectionMode="single": items are radios. The chosen row is the ink pill with a check, and a choice closes the menu.selectionMode="multiple": items are checkboxes. A chosen row shows only the check, and the menu stays open.shouldCloseOnSelectoverrides closing on the menu, a section or one item.
Every row in a selectable list reserves room for the check, so the width never changes.
Example
Example
import { Button, Menu, MenuItem, MenuSection, MenuSeparator, MenuTrigger } from '@snapshot-ui/react';
import { Monitor, Moon, Sun } from 'lucide-react';
import type { ReactElement } from 'react';
export default function MenuSelection(): ReactElement {
return (
<MenuTrigger>
<Button variant="secondary">View options</Button>
<Menu>
<MenuSection title="Theme" selectionMode="single" defaultSelectedKeys={['system']}>
<MenuItem id="light" icon={<Sun />}>
Light
</MenuItem>
<MenuItem id="dark" icon={<Moon />}>
Dark
</MenuItem>
<MenuItem id="system" icon={<Monitor />} description="Follows the device">
System
</MenuItem>
</MenuSection>
<MenuSeparator />
<MenuSection title="Show" selectionMode="multiple" defaultSelectedKeys={['sidebar']}>
<MenuItem id="sidebar">Sidebar</MenuItem>
<MenuItem id="minimap">Minimap</MenuItem>
</MenuSection>
</Menu>
</MenuTrigger>
);
}Links
An item with href is a real link, an anchor with role="menuitem", so a middle or modified click opens it as the browser would. A plain press navigates through React Aria's RouterProvider, as Link does. One provider at the app's root routes every link, set up in Routing.
To render your router's own link, use the item's inherited render prop in a client component. An item without href renders a div:
<MenuItem
id="settings"
href="/settings"
render={(props) => ('href' in props ? <NextLink {...props} href={props.href} /> : <div {...props} />)}
>
Settings
</MenuItem>Example
Example
'use client';
import { Button, Menu, MenuItem, MenuTrigger, RouterProvider } from '@snapshot-ui/react';
import { useRouter } from 'next/navigation';
import type { ReactElement } from 'react';
const PAGES = [
{ slug: 'button', title: 'Button' },
{ slug: 'dialog', title: 'Dialog' },
{ slug: 'popover', title: 'Popover' },
];
export default function MenuLinks(): ReactElement {
const router = useRouter();
// An app wraps its whole tree once; the example wraps only itself.
return (
<RouterProvider
navigate={(href) => {
router.push(href);
}}
>
<MenuTrigger>
<Button variant="secondary">Jump to a component</Button>
<Menu>
{PAGES.map(({ slug, title }) => (
<MenuItem key={slug} id={slug} href={`/docs/components/${slug}`}>
{title}
</MenuItem>
))}
</Menu>
</MenuTrigger>
</RouterProvider>
);
}Submenus
Put a MenuItem and then a Menu in a SubmenuTrigger for an item that opens a submenu. The item draws a chevron after its end value, in the key hint's place, and keeps its wash while the submenu is open, so you can see where it came from. The submenu opens beside the row at its end, its first row level with the row, and on the other side where there is no room; where neither side has room, as on a narrow phone, it opens under the row, over the rows below it.
Arrow Right opens it, or a press, or a pause of the pointer over the row, 200ms unless SubmenuTrigger's delay says otherwise; Arrow Left or Escape closes that level alone, with focus back on the row, and the menu takes the Escape, so a dialog around it stays open until the next one; a choice in it closes every level and gives focus back to the menu's trigger. React Aria takes the open and close arrows from the locale, so a right-to-left page sets I18nProvider with its locale and the two arrows swap. For the reader's theme, ThemeMenu is a submenu ready made.
Example
Example
In Inbox
'use client';
import {
IconButton,
Menu,
MenuItem,
MenuSeparator,
MenuTrigger,
SubmenuTrigger,
Text,
type Key,
} from '@snapshot-ui/react';
import { Copy, Ellipsis, FolderInput, Trash2 } from 'lucide-react';
import { useState, type ReactElement } from 'react';
const FOLDERS: Record<string, string> = { inbox: 'Inbox', receipts: 'Receipts', taxes: 'Taxes' };
/** A Move to item opening a submenu of folders: Arrow Right opens it, Arrow Left or Escape closes it. */
export default function MenuSubmenu(): ReactElement {
const [folder, setFolder] = useState<Key>('inbox');
return (
<div style={{ display: 'grid', gap: 16, justifyItems: 'center' }}>
<MenuTrigger>
<IconButton aria-label="Document actions" variant="outline">
<Ellipsis />
</IconButton>
<Menu>
<MenuItem id="duplicate" icon={<Copy />}>
Duplicate
</MenuItem>
<SubmenuTrigger>
<MenuItem id="move" icon={<FolderInput />} end={FOLDERS[String(folder)]}>
Move to
</MenuItem>
<Menu
selectionMode="single"
selectedKeys={[folder]}
disallowEmptySelection
onSelectionChange={(keys) => {
const [next] = keys === 'all' ? [] : [...keys];
if (next !== undefined) setFolder(next);
}}
>
{Object.entries(FOLDERS).map(([id, name]) => (
<MenuItem key={id} id={id}>
{name}
</MenuItem>
))}
</Menu>
</SubmenuTrigger>
<MenuSeparator />
<MenuItem id="delete" icon={<Trash2 />} variant="danger">
Delete
</MenuItem>
</Menu>
</MenuTrigger>
<Text>In {FOLDERS[String(folder)]}</Text>
</div>
);
}Usage
Put a button and a Menu in a MenuTrigger. To control it, pass isOpen, defaultOpen and onOpenChange to MenuTrigger. Menu is a client component, and a server component can render it, as the selection example does with defaultSelectedKeys. A menu that runs a handler or keeps its choice in state needs a client component, as the other two examples do.
onActionon the menu receives theidof the item that ran. An item's ownonActionruns first, with no argument.- A danger item usually asks first. Pair it with a Confirm dialog whose confirm button names the same outcome.
- The popover opens under the button at its start edge, and flips where the screen has no room.
placementchanges that. - The popover is at least 12rem wide, or the button's width if wider. A long menu scrolls inside it.
- A menu opened from a dark card is dark on a light page, as Theming explains.
Props
ref and className go on the menu element, and popoverClassName on the popover around it. The rest of React Aria's menu props, such as items with a render function and renderEmptyState, pass through.
className ClassNameOrFunction<MenuRenderProps> | Goes on the menu element: a string, or a function of the menu's state ( |
|---|---|
defaultSelectedKeys "all" | Iterable<Key> | The initial selected keys in the collection (uncontrolled). |
disabledKeys Iterable<Key> | The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. |
onAction ((key: Key, value: object) => void) | Handler that is called when an item is selected. |
onSelectionChange ((keys: Selection) => void) | Handler that is called when the selection changes. |
placement Placement | Where the popover opens. Default: 'bottom start', and 'end top' for a submenu |
popoverClassName string | Goes on the popover around it. |
selectedKeys "all" | Iterable<Key> | The currently selected keys in the collection (controlled). |
selectionMode SelectionMode | The type of selection that is allowed in the collection. |
shouldCloseOnSelect boolean | Whether the menu should close when the menu item is selected. |
children ReactNode | The label. A string is also the item's text value, for typeahead and announcement. |
|---|---|
className string | ((values: MenuItemRenderProps & { defaultClassName: string | undefined; }) => string) | |
description ReactNode | A second line under the label, in slate. It becomes the item's accessible description. |
end ReactNode | A preformatted value at the end, such as an amount, before the key hint. Shown as given. It describes the item and is not part of its name. |
href string | A URL to link to. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#href). |
icon ReactNode | A 16px icon at the start, hidden from assistive technology. |
id Key | The unique id of the item. |
isDisabled boolean | Whether the item is disabled. |
onAction (() => void) | Handler that is called when the item is selected. |
shortcut string | A key hint at the end, such as "Q" or "Mod ,", drawn as keys. Separate keys with spaces. "Mod" shows as Cmd or Ctrl once mounted. |
shouldCloseOnSelect boolean | Whether the menu should close when the menu item is selected. |
textValue string | A string representation of the item's contents, used for features like typeahead. |
variant "danger" | "default" |
Default: 'default' |
className string | |
|---|---|
defaultSelectedKeys "all" | Iterable<Key> | The initial selected keys in the collection (uncontrolled). |
onSelectionChange ((keys: Selection) => void) | Handler that is called when the selection changes. |
selectedKeys "all" | Iterable<Key> | The currently selected keys in the collection (controlled). |
selectionMode SelectionMode | The type of selection that is allowed in the collection. |
shouldCloseOnSelect boolean | Whether the menu should close when the menu item is selected. |
title ReactNode | The heading above the section's items, in the overline style. |
className string | |
|---|---|
ref Ref<HTMLElement> |
Accessibility
A true role="menu", named by its trigger, which says that it opens a menu and whether it is open. Items are menu items, or radios and checkboxes in single and multiple selection. Each is named by its label and described by its second line and key hint.
- A disabled item is announced as unavailable, and the arrows skip it.
- Opened from the keyboard, focus starts on the first item, or the last with Arrow Up. Opened with a pointer, it starts on the list, or on the chosen item when the menu selects.
- Focus stays in the open menu until a choice, Escape or a press outside closes it, then returns to the trigger.
| DownUp | Move between items, stopping at the first and the last. |
|---|---|
| HomeEnd | Jump to the first or last item. |
| A-Z | Jump to an item by its first letters. |
| EnterSpace | Activate the item, or choose it. |
| Right | On an item with a submenu, open it with focus on its first item, or its chosen one. Left in right to left. |
| Left | In a submenu, close it and return focus to its item. Right in right to left. |
| Escape | Closes the menu, or a submenu alone, and returns focus. |
| Tab | Stays in the open menu. |
Design rules
What keeps menus in the system.
Say the action in a verb, such as Rename or Duplicate.
Group related items in a section with a heading.
Put a danger item last, after a separator.
Never nest a menu in a menu.
Never use a menu for navigation that a list on the page could show.
Never mark danger by color alone. Name what is lost, such as Delete the project.