Skip to main content
BM

Menu

A dropdown of actions, links and options behind a button.

View source on GitHub

MenuExample.tsx
import { Menu, MenuItem, MenuPopup, MenuTrigger } from "@/components/Menu";

export function MenuExample() {
  return (
    <Menu>
      <MenuTrigger iconEnd="chevron-down">Project actions</MenuTrigger>
      <MenuPopup>
        <MenuItem icon="copy">Copy link</MenuItem>
        <MenuItem>Download</MenuItem>
        <MenuItem disabled>Delete</MenuItem>
      </MenuPopup>
    </Menu>
  );
}

Examples

Actions

Last action: nothing yet.

Last action: nothing yet.

Radio group (controlled)

Sorted by newest.

Checkboxes and a separator

Descriptions shown; archived hidden.

Space toggles an option and keeps the menu open; Enter or a click closes it.

Trigger looks

Placement

Controlled open

The menu is closed.

Customized with component tokens

CSS
.compactMenu {
  --component-menu-item-font-size: var(--type-size-small);
  --component-menu-item-checked-indicator: var(--color-accent-2-text);
}

Props

Any native prop not listed here goes to the root element unchanged.

Menu props
PropTypeDefaultDescription
openboolean—Controlled open state; pair with onOpenChange.
onOpenChange(open: boolean) => void—Called on every requested open or close, controlled or not. An outside press, Escape and Tab always request closing.
MenuTrigger (same as Button) props
PropTypeDefaultDescription
variant"primary" | "secondary" | "ghost""secondary"Emphasis: one primary action per view, secondary for the rest, ghost for toolbars.
size"sm" | "md""md"32px or 40px tall.
iconStartIconName—Decorative icon before the label.
iconEndIconName—Decorative icon after the label.
type"button" | "submit" | "reset""button"Defaults to "button", not the native "submit".
disabledbooleanfalseSets aria-disabled, not the native attribute: still focusable, announced, and never activated.
aria-pressedboolean | "mixed"—Makes it a toggle button. "mixed" has no look of its own.
disabledReasonReactNode—While disabled: why. It's the button's accessible description, and shows as a tooltip on hover, keyboard focus, and a press or tap.
nameTooltipbooleantrueIcon-only buttons named by aria-label or VisuallyHidden show that name as a tooltip; false turns it off.
MenuPopup props
PropTypeDefaultDescription
side"bottom" | "top""bottom"The side of the trigger it opens on. It still flips when it doesn't fit.
align"start" | "end""start"The trigger edge it lines up with. It still shifts to stay in the viewport.
MenuItem props
PropTypeDefaultDescription
onSelect() => void—Runs on activation, before the menu closes (and before a link is followed).
disabledbooleanfalseShown and announced as unavailable; focusable but never activated.
iconIconName—Decorative icon before the label.
closeOnSelectboolean—true always closes the menu on choosing this item, false never does. By default Enter and click close; Space closes only on action and link items.
hrefstring—Makes it a link item (<a role="menuitem">). A /path routes client-side; modifier-clicks work as on any link.
newTabbooleanfalseWith href: opens in a new tab, with an icon and “, opens in new tab” in its name.
relstring—With href: merged with noopener noreferrer for newTab.
MenuCheckboxItem props
PropTypeDefaultDescription
checkedboolean—Controlled state; pair with onCheckedChange.
defaultCheckedbooleanfalseUncontrolled initial state.
onCheckedChange(checked: boolean) => void—Called with the new state.
iconIconName—Decorative icon before the label.
disabledbooleanfalseShown and announced as unavailable; focusable but never activated.
closeOnSelectboolean—true always closes the menu on choosing this item, false never does. By default Enter and click close; Space closes only on action and link items.
MenuRadioGroup props
PropTypeDefaultDescription
labelstring—Required. The group's accessible name, shown small.
valuestring—Controlled value; pair with onValueChange.
defaultValuestring—Uncontrolled initial value.
onValueChange(value: string) => void—Called with the chosen value.
MenuRadioItem props
PropTypeDefaultDescription
valuestring—Required. The value this item selects.
iconIconName—Decorative icon before the label.
disabledbooleanfalseShown and announced as unavailable; focusable but never activated.
closeOnSelectboolean—true always closes the menu on choosing this item, false never does. By default Enter and click close; Space closes only on action and link items.

Component tokens

Menu component tokens
TokenCopyDescriptionAliasesLightDarkPreview
--component-menu-popup-backgroundPopup surface.color.surface.raised#fffffffrom color.gray.0#18202efrom color.slate.900
--component-menu-popup-borderPopup outline.color.border.subtle#e5dccbfrom color.sand.200#272f3ffrom color.slate.800
--component-menu-popup-radiusPopup corners.radius.surface4pxfrom radius.44pxfrom radius.4
--component-menu-popup-elevationPopup shadow, so it reads above the page.effect.elevation.overlay0px 4px 12px 0px rgb(0% 0% 0% / 0.12)from effect.shadow.md0px 4px 12px 0px rgb(0% 0% 0% / 0.4)from effect.shadow.md-dark
--component-menu-popup-paddingPadding around the items.space.inset.sm0.5remfrom space.20.5remfrom space.2
--component-menu-popup-min-widthMinimum popup width.size.column.sm12remfrom size.4812remfrom size.48
--component-menu-popup-border-widthPopup outline width.effect.border.width2pxfrom effect.line.22pxfrom effect.line.2
--component-menu-popup-offsetGap between the popup and its trigger (and the viewport edge in the JS fallback).space.stack.sm0.5remfrom space.20.5remfrom space.2
--component-menu-popup-slide-distanceHow far the popup slides in from its trigger's side as it opens.space.stack.xs0.25remfrom space.10.25remfrom space.1
--component-menu-item-textItem text.color.text.default#17202bfrom color.gray.900#fffffffrom color.gray.0
--component-menu-item-background-focusItem background while focused (pointing at an item focuses it).color.surface.sunken#f2ede2from color.sand.100#060a13from color.slate.1000
--component-menu-item-text-focusItem text while focused.color.text.default#17202bfrom color.gray.900#fffffffrom color.gray.0
--component-menu-item-focus-ring-colorFocus indicator, drawn inside the item.color.focus.ring#005f7bfrom color.cyan.700#54d8f4from color.cyan.300
--component-menu-item-padding-inlineInline padding of an item.space.inline.sm0.5remfrom space.20.5remfrom space.2
--component-menu-item-heightMinimum block size of an item (32px, above the 24px target minimum).size.control.sm2remfrom size.82remfrom size.8
--component-menu-item-radiusItem corners.radius.control4pxfrom radius.44pxfrom radius.4
--component-menu-item-gapGap between an item's icons and label.space.inline.sm0.5remfrom space.20.5remfrom space.2
--component-menu-item-checked-indicatorCheck mark of a checked radio or checkbox item (3:1, it carries state).color.action.background#b37500from color.gold.600#f5e3c2from color.champagne.100
--component-menu-item-disabled-textText of a disabled item.color.text.subtle#5d6876from color.gray.600#d3dce6from color.slate.200
--component-menu-item-font-familyItem font, so items don't inherit the text the Menu sits in.type.family.body'Inter Variable', 'Inter Fallback', system-ui, -apple-system, 'Segoe UI', Roboto, sans-seriffrom type.font-stack.sans'Inter Variable', 'Inter Fallback', system-ui, -apple-system, 'Segoe UI', Roboto, sans-seriffrom type.font-stack.sans
--component-menu-item-font-sizeItem font size.type.size.body1remfrom type.font-size.md1remfrom type.font-size.md
--component-menu-item-line-heightItem line height.type.leading.body1.6from type.line-height.normal1.6from type.line-height.normal
--component-menu-item-font-weightItem font weight.type.weight.body400from type.font-weight.400400from type.font-weight.400
--component-menu-group-gapSpace between a group and the items or groups beside it.space.stack.sm0.5remfrom space.20.5remfrom space.2
--component-menu-group-label-textRadio group label.color.text.subtle#5d6876from color.gray.600#d3dce6from color.slate.200
--component-menu-group-label-font-familyRadio group label font (mono).type.family.label'Fira Code Variable', 'Fira Code Fallback', ui-monospace, SFMono-Regular, Menlo, Consolas, monospacefrom type.font-stack.mono'Fira Code Variable', 'Fira Code Fallback', ui-monospace, SFMono-Regular, Menlo, Consolas, monospacefrom type.font-stack.mono
--component-menu-group-label-font-sizeRadio group label size.type.size.label0.75remfrom type.font-size.xs0.75remfrom type.font-size.xs
--component-menu-group-label-trackingRadio group label letter spacing.type.tracking.label0.16emfrom type.letter-spacing.160.16emfrom type.letter-spacing.16
--component-menu-group-label-padding-blockBlock padding of a group label.space.stack.xs0.25remfrom space.10.25remfrom space.1
--component-menu-separator-colorSeparator line (decorative).color.border.subtle#e5dccbfrom color.sand.200#272f3ffrom color.slate.800
--component-menu-separator-thicknessSeparator line thickness.effect.border.width2pxfrom effect.line.22pxfrom effect.line.2
--component-menu-separator-margin-blockSpace above and below a separator.space.stack.xs0.25remfrom space.10.25remfrom space.1
--component-menu-transition-durationDuration of the popup's fade (0ms under reduced motion).motion.duration.fast150msfrom motion.time.150Reduced motion: 0ms150msfrom motion.time.150No preview
--component-menu-transition-easingEasing of the popup's fade.motion.easing.entercubic-bezier(0, 0, 0.2, 1)from motion.curve.outcubic-bezier(0, 0, 0.2, 1)from motion.curve.outNo preview

Accessibility

  • WAI-ARIA menu button: the trigger has aria-haspopup="menu" and aria-expanded; the popup is role="menu", named by the trigger.
  • Pointing at an item focuses it, so only one item is ever highlighted, and the keyboard continues from there.
  • Checkbox items are menuitemcheckbox and radio items menuitemradio, each with aria-checked and a visible check mark; radio items sit in a group named by its label.
  • Separators are role="separator" and the keyboard skips them.
  • Link items are real links (<a role="menuitem">): new tabs and modifier-clicks work, and a new-tab item's name ends “, opens in new tab”.
  • Only the focused item is in the tab sequence; disabled items stay focusable but never activate.
  • A controlled menu opens and closes through onOpenChange; an outside press, Escape and Tab always request closing.
  • The focus ring is component.menu.item.focus-ring-color, drawn inside the item.
  • In forced colors, the focused item uses Highlight and a disabled item GrayText; a focused disabled item keeps GrayText and gets a Highlight ring instead.
  • The popup is in the top layer, so overflow can't clip it, and it flips to stay in the viewport.
Menu keyboard
KeyAction
Enter / Space / clickOn the trigger: opens, focusing the checked item or the first.
Down Arrow / Up ArrowOn the trigger: opens on the first or last item. In the menu: next or previous item, wrapping.
Home / EndFirst or last item.
A to ZNext item starting with that letter.
EnterChooses the item (or follows the link), closes, and returns focus to the trigger.
SpaceChooses an action or link item and closes. Toggles a checkbox or radio item and keeps the menu open.
EscapeCloses and returns focus to the trigger.
TabCloses and moves focus on as usual.