Skip to main content

Design system components

Menu

Radix dropdown-menu primitive set (Menu, Trigger, Content, Item, Sub) shared by Avatar, SplitButton, Breadcrumb and FieldShell.

Status: stable. Group: overlay. Markdown version: /design-system/components/menu.md

When to use

Menu is the shared dropdown-menu primitive: a focusable trigger opens a portaled list of actions. It is built on @radix-ui/react-dropdown-menu, so roving focus, arrow-key navigation, typeahead, Escape, focus return and collision-aware positioning all come from Radix; this layer owns only the visual treatment. Compose it from parts: wrap your control in MenuTrigger asChild, list MenuItems inside MenuContent, group with MenuSeparator and nest a MenuSub for second-level menus. Items take a leading icon (rendered in a fixed gutter so labels align), end-aligned meta content (the deprecated trailing prop maps onto meta), an optional trailingIcon, a selected check indicator, tone="destructive" for deletions, a disabled flag and either an onSelect handler or an href to render as a link. Setting selected (true or false) makes the item a checkable choice: it renders as role=menuitemcheckbox with a matching aria-checked, so the state the check draws is announced too; a selected href item stays a link marked aria-current=true. It is the dogfooded engine behind Avatar's account menu, SplitButton's options, Breadcrumb's collapsed items and FieldShell's field action menus; NavMenuList stays a navigation list, not a menu.

When not to use

  • Do not reach for Menu to build primary site navigation; navigation is a list of links (NavMenuList), not an actions menu; forcing role=menu onto nav is an a11y regression.
  • Do not put a Menu trigger inside another interactive control; give the menu its own focusable trigger via MenuTrigger asChild.
  • Do not hand-roll dropdown positioning or keyboard handling around Menu; Radix owns collision, roving focus, typeahead, and Escape.
  • Do not use Menu for primary navigation; a list of page links belongs in NavMenuList with aria-current, not a role=menu.

Look-alikes

  • NavMenuList: Do not reach for Menu to build primary site navigation; navigation is a list of links (NavMenuList), not an actions menu; forcing role=menu onto nav is an a11y regression.

Props

Menu props
PropTypeDefaultDescriptionRequired
childrenReact.ReactNodenoneNo description in the contract.Yes
classNamestringnoneExtra class on the content surface.No
side"top" | "right" | "bottom" | "left"nonePreferred side relative to the trigger; flips when out of room (fixed collisionPadding of 12px). Defaults to "bottom".No
align"center" | "start" | "end"startAlignment along the trigger edge. Defaults to "start".No
sideOffsetnumbernoneGap in px between the trigger and the content. Defaults to 6.No
alignOffsetnumbernoneShift in px along the alignment edge (negative moves toward the start). Defaults to 0.No

Variants

  • Allowed align values: start, center, end (default start).
  • Allowed side values: top, right, bottom, left.

Invalid combinations

The contract records no invalid combinations.

Keyboard

Menu keyboard behaviour
KeyBehaviour
ArrowDownMoves focus or the value in that direction. (standard)
ArrowUpMoves focus or the value in that direction. (standard)
HomeMoves to the first item. (standard)
EndMoves to the last item. (standard)
EnterActivates the focused control. (standard)
EscapeCloses or dismisses the open layer. (standard)
ArrowRightMoves focus or the value in that direction. (standard)
ArrowLeftMoves focus or the value in that direction. (standard)
TabMoves focus to the control and on to the next focusable element. (standard)

Rows marked (standard) name a key the contract lists without a description; the behaviour shown is the usual one for that key.

Rules

Every item under When not to use is a MUST NOT. MUST and SHOULD follow RFC 2119.

Menu rules
LevelRuleHow it is checked
MUSTARIA: Trigger exposes aria-haspopup=menu and aria-expanded (Radix)Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: Content is role=menu; items are role=menuitem with roving tabindex, or role=menuitemcheckbox when selected is set.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: ArrowUp/Down move between items, Home/End jump, typeahead focuses by label.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: Escape closes and returns focus to the trigger; Tab closes (non-modal menu-button pattern)Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: Submenus open with ArrowRight/Enter and close with ArrowLeft/Escape.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: Disabled items are announced disabled and are not selectable.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: MenuItem with selected set (true or false) is role=menuitemcheckbox with a matching aria-checked; the drawn check stays aria-hidden because aria-checked carries the state. A selected href item stays a link and carries aria-current=true.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTRequired stories produce no axe violations at the configured severity.Automated by npm run test:stories:matrix:ci. WCAG 2.5.8.
MUSTThe accessibility tree (role, name, state) matches the committed snapshot for every required story.Automated by npm run test:stories:matrix:ci. WCAG 1.3.1, 4.1.2.
MUSTThe component remains legible and operable under a real browser forced-colors: active pass.Automated by npm run test:stories:hc.
MUSTEvery documented keyboard interaction works as specified.Not yet verified: no evidence record. WCAG 2.1.1, 2.1.2.
MUSTA human completed an end-to-end accessibility review of this component.Manual review, last on 2026-11-04.
SHOULDWrap your real Button or Avatar in MenuTrigger asChild so it receives the trigger wiring instead of a nested extra button.Not stated in the contract.
SHOULDGive each MenuItem a short verb label; put the icon in the icon slot so labels align in a column regardless of glyph width.Not stated in the contract.
SHOULDUse href for navigation items (renders an anchor, so open-in-new-tab and middle-click work) and onSelect for actions.Not stated in the contract.
SHOULDGroup related actions with MenuSeparator and reserve MenuSub for genuinely nested choices; a flat menu is faster to scan.Not stated in the contract.
SHOULD NOTDo not re-implement positioning or keyboard handling around Menu; Radix already provides collision, roving focus, typeahead, and Escape.Not stated in the contract.

Install and import

  • Install: npm install @digitaltableteur/react (version 0.9.1, restricted access on npm).
  • Import: import { Menu } from "@digitaltableteur/react";
  • Entry point: @digitaltableteur/react/navigation exports it too.
  • Styles: import @digitaltableteur/react/style.css once at the app boundary.
  • Also available as the custom element <dt-menu>; see the custom elements manifest.