Skip to content

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/tokens
import { 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

KeysAction
EnterorSpaceor↓On the trigger: opens the menu and focuses the first item (↑ focuses the last).
↑or↓Moves between items, skipping disabled ones.
HomeorEndJumps to the first / last item.
TypingMoves to the next item starting with the typed characters.
→ (← in RTL)Opens a submenu; ← (→ in RTL) closes it.
EnterorSpaceActivates the item (or toggles it in selection mode).
EscapeCloses 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 / itemsRequired
ReactNode | (item) => ReactElement / Iterable<T>

MenuItem, MenuSection, MenuSeparator and SubmenuTrigger elements, or a render function over items.

onAction
(key: Key) => void

Called with the id of the activated item. The menu then closes.

selectionMode / selectedKeys / onSelectionChange
'none' | 'single' | 'multiple' / Iterable<Key> / (keys: Selection) => void

Makes items checkable; selected items show a checkmark. Can also be set per MenuSection.

placement
Placement

Where the menu opens relative to its trigger. Submenus open at the end edge.

Default 'bottom start'

disabledKeys
Iterable<Key>

Items that can't be focused or activated.

renderEmptyState
() => ReactNode

Content when there are no items.

MenuItem

childrenRequired
ReactNode

The label. Pass textValue when it isn't a plain string (used for typeahead).

id
Key

Passed to onAction and used for selection.

icon
ReactNode

Leading icon. Decorative.

description
ReactNode

Second line under the label, announced as its description.

shortcut
string

Shortcut hint at the inline end, e.g. "⌘E". Display only — bind the keys yourself.

tone
  • neutraldefault
  • danger

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 / onAction
boolean / string / () => void

Standard React Aria MenuItem props.

MenuSection

title
ReactNode

Visible group heading; also the group's accessible name.

MenuTrigger

childrenRequired
ReactNode

A pressable trigger followed by a Menu.

trigger
  • pressdefault
  • longPress

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