Menu
A list of actions or options that opens from a button, with icons, shortcuts, sections, submenus and selection.
Preview
Installation
pnpm add @syntara/react @syntara/tokensimport { Menu, MenuItem, MenuSection, MenuSeparator, MenuTrigger, SubmenuTrigger } from '@syntara/react';Usage
import { Button, Menu, MenuItem, MenuSeparator, MenuTrigger } from '@syntara/react';
export function Actions() {
return (
<MenuTrigger>
<Button variant="outline">Actions</Button>
<Menu onAction={(key) => console.log(key)}>
<MenuItem id="edit" shortcut="⌘E">Edit</MenuItem>
<MenuItem id="duplicate">Duplicate</MenuItem>
<MenuSeparator />
<MenuItem id="delete" tone="danger">Delete</MenuItem>
</Menu>
</MenuTrigger>
);
}Examples
Sections and shortcuts
Section titles, shortcut hints, descriptions and a disabled item.
Submenus
SubmenuTrigger wraps an item and the Menu it opens.
Selection
Single and multiple selection per section, shown with checkmarks.
Accessibility
| Keys | Action |
|---|---|
| EnterorSpaceor↓ | On the trigger: opens the menu and focuses the first item (↑ focuses the last). |
| ↑or↓ | Moves between items, skipping disabled ones. |
| HomeorEnd | Jumps to the first / last item. |
| Typing | Moves to the next item starting with the typed characters. |
| → (← in RTL) | Opens a submenu; ← (→ in RTL) closes it. |
| EnterorSpace | Activates the item (or toggles it in selection mode). |
| Escape | Closes the menu and returns focus to the trigger. |
- role="menu" with menuitem, menuitemradio or menuitemcheckbox items depending on selectionMode.
- Shortcut hints are rendered in <kbd> and announced as part of the item’s description (aria-describedby).
- Danger items are marked by colour and should also say what they do ("Delete claim").
- Overlays render in a portal on <body>, outside any ThemeScope. When it opens, the overlay looks up data-syntara-theme, data-syntara-scheme, data-syntara-density, dir and lang — each on the nearest ancestor that has it — starting from its trigger (or from where the component is rendered, when controlled), and copies them onto its own root — so it uses the same tenant tokens and direction as the page region it came from, and nested overlays inherit from their parent overlay.
- Right-to-left: give the ThemeScope a locale (e.g. locale="ar-AE"), which sets dir and wraps React Aria's I18nProvider, so arrow keys and start/end placement flip as well as the layout.
- The menu is glass. The highlighted item gets an opaque surface.selected background (feedback.danger.bg for danger items) that fades and springs in, so every coloured text sits on a solved pair; descriptions and shortcuts switch to the label colour on that row, because text.subtle isn't solved against surface.selected. Items step in one after another as the menu opens (first 8, 30ms apart); pressed items scale to 0.96 and spring back; none of this moves with prefers-reduced-motion.
Guidelines
Do
- Start labels with a verb and keep them short.
- Put destructive items last, after a separator.
- Group related items in titled sections.
Don’t
- Don't nest submenus more than one level.
- Don't use a Menu for navigation between pages — use links or tabs.
- Don't use a Menu to pick a form value — use Select.
API reference
Menu
children / itemsRequiredReactNode | (item) => ReactElement / Iterable<T>MenuItem, MenuSection, MenuSeparator and SubmenuTrigger elements, or a render function over items.
onAction(key: Key) => voidCalled with the id of the activated item. The menu then closes.
selectionMode / selectedKeys / onSelectionChange'none' | 'single' | 'multiple' / Iterable<Key> / (keys: Selection) => voidMakes items checkable; selected items show a checkmark. Can also be set per MenuSection.
placementPlacementWhere the menu opens relative to its trigger. Submenus open at the end edge.
Default
'bottom start'disabledKeysIterable<Key>Items that can't be focused or activated.
renderEmptyState() => ReactNodeContent when there are no items.
MenuItem
childrenRequiredReactNodeThe label. Pass textValue when it isn't a plain string (used for typeahead).
idKeyPassed to onAction and used for selection.
iconReactNodeLeading icon. Decorative.
descriptionReactNodeSecond line under the label, announced as its description.
shortcutstringShortcut hint at the inline end, e.g. "⌘E". Display only — bind the keys yourself.
toneneutraldefaultdanger
danger marks a destructive action: a red icon at rest, and a red row (feedback.danger.bg + feedback.danger.fg) while highlighted. The label stays text.default at rest because feedback colours aren't solved against glass.
isDisabled / href / onActionboolean / string / () => voidStandard React Aria MenuItem props.
MenuSection
titleReactNodeVisible group heading; also the group's accessible name.
MenuTrigger
childrenRequiredReactNodeA pressable trigger followed by a Menu.
triggerpressdefaultlongPress
How the menu opens.
SubmenuTrigger
childrenRequired[MenuItem, Menu]The item that opens the submenu, then the submenu.
Tokens
The semantic tokens this component reads, grouped by what they control. Swatches show this site’s theme; change a tenant’s brand and the component follows with no code change.
- Colour10
surface.selectedtext.defaultfocus.ringfeedback.danger.bgfeedback.danger.fgtext.disabledtext.subtleborder.subtleborder.defaultsurface.raised- Type8
font.size.mdline-height.snugfont.size.xsline-height.normalfont.bodyfont.weight.mediumfont.tracking.mdfont.tracking.xs- Space and size8
space.1space.0space.16space.2control-heightspace.4card-insetspace.3- Shape2
radius.containerradius.badge- Depth4
glass.blurshadow.overlayhairlineglass.opacity- Motion6
motion.duration.fastmotion.easingmotion.duration.springmotion.springmotion.duration.normalmotion.easing-out- Other2
sheenrim