Designer view
Menu Button
A button that opens a menu of actions or commands — a more document item, a settings list, an overflow ("…") menu of context actions. Distinct from Popover (interactive arbitrary content) by its `role="menu"` semantic and APG menu-keyboard contract; distinct from Select (single-value commit) by invoking actions rather than selecting values; distinct from navigation menus by being a transient action surface, not a persistent navigation region.
Also called Split button Dropdown button
When to use
Use
For a list of commands or actions invoked from a single button — More-actions menus, overflow ("⋯") menus, user avatar menus, context-action menus, settings dropdowns. The user opens the menu, picks one command, and the menu closes.
Avoid
For arbitrary interactive content (forms, multi-step flows) — that is `Popover`. For single-value selection from a fixed list — that is `Select`. For navigation between independent pages — that is `SidebarNav` or a navigation list of `Link` elements. For long lists of commands — switch to a Command Palette (see performance threshold).
Versus related
- popover
`Popover` hosts arbitrary interactive content with `role="dialog"`; `MenuButton` hosts a list of commands with `role="menu"` and the APG menu-keyboard contract. The role choice determines the keyboard behaviour: Popover allows Tab into content; MenuButton uses ArrowKeys with Tab closing the menu.
- select
`Select` commits a value (the user picks one from a list); `MenuButton` invokes an action (the user runs one command). Select's `aria-haspopup` is "listbox"; MenuButton's is "menu". Their keyboard contracts differ slightly (Select stays focused on trigger via `aria-activedescendant`; MenuButton moves DOM focus into the menu).
- sidebar-nav
`SidebarNav` is a persistent navigation region with anchors as children; `MenuButton` is a transient action surface invoked on demand. SidebarNav lives in the page layout; MenuButton is portal-mounted and dismisses after one action.
- tooltip
`Tooltip` is non-interactive descriptive text revealed on hover/focus; `MenuButton` opens a list of interactive commands invoked on click/keyboard. They do not share visual or behavioural surface area.
- button
`Button` invokes one action on activation; `MenuButton` reveals a popup list of related actions and lets the user pick. Reach for MenuButton when more than ~3 commands share a single trigger surface (overflow menus, contextual actions, settings groups); a plain Button is correct when there is exactly one canonical action.
- link
`Link` navigates to a URL; `MenuButton` opens an in-page menu of commands that do not change the URL. A common confusion is a "user menu" that mixes both — canon: render the trigger as a `MenuButton`, render the navigation entries inside as `Link` children.
- menu
`MenuButton` combines a trigger button with a menu — the button activates the menu, the menu hosts the commands. `Menu` is the menu surface alone, composable independently of any specific trigger (right-click context menus, embedded action lists, sub-menus of a parent menu). MenuButton internally renders a Menu; Menu canonicalises the list-of-commands pattern for the cases where the trigger is not a button (right- click contextmenu, embedded permanent menu, programmatic opening from a non-button trigger).
Menu Button pairs a trigger with a popup menu of items — the canonical pattern for action menus, overflow menus, and context-sensitive command lists. It implements the APG menu-button role with aria-haspopup and aria-expanded on the trigger, role=menu on the popup, and roving tabindex over menu items. The reference covers the keyboard contract that distinguishes activation from navigation, the divergence from Combobox (filtered text input) and Select (form-bound value), and the dismiss-reason vocabulary shared with Popover.
Figma anatomy
| Slot | Figma type | Hint |
|---|---|---|
trigger | instance | Button instance with optional caret or icon-only treatment; menu state via component property |
caret | instance | Icon component instance; rotation bound to expanded state |
menu | frame | Floating frame; min-inline-size matches trigger by default |
menu-item | instance | Menu item instance; supports leading icon, label, optional shortcut, optional trailing indicator |
separator | rectangle | 1px horizontal line; padding inset from container edges |
Token usage per slot
trigger- spacing
- padding
spacing.compact - gap
spacing.compact
- padding
- radius
- corner
radius.md
- corner
- color
- ring
color.border.focus
- ring
caret- color
- foreground
color.text.muted
- foreground
menu- spacing
- padding
spacing.tight
- padding
- radius
- corner
radius.md
- corner
- color
- background
color.surface.raised - border
color.border.subtle
- background
- elevation
- shadow
elevation.lg
- shadow
menu-item- spacing
- padding
spacing.compact - gap
spacing.compact
- padding
- radius
- corner
radius.sm
- corner
- color
- foreground
color.text.primary
- foreground
- typography
- size
text.sm
- size
separator- color
- background
color.border.subtle
- background
Figma ↔ Code property map
| Figma | Kind | Code | Notes |
|---|---|---|---|
Variant | Enum | variant | Maps standard / icon-only. |
Side | Enum | side | top / right / bottom / left. Authored placement preference; auto-flips on viewport collision. |
Align | Enum | align | start / center / end along the perpendicular axis. |
Size | Enum | size | sm / md / lg. Affects trigger and menu typography. |
Has Caret | Boolean | hasCaret | Toggles the caret slot. Default true for standard variant; default false for icon-only. |
Has Separators | Boolean | separators | Toggles whether menu groups are visually separated. Decorative — does not change keyboard behaviour. |
Item Count | Enum | items.length | Figma exposes 2/3/4/5/6+ item counts as a Variant for preview-time layout review. Code accepts an array of menu item definitions. |
Trigger Label | Text | triggerLabel | For standard variant. Icon-only variant uses aria-label instead. |
Trigger Icon | Slot | triggerIcon | For icon-only variant. Common: kebab (⋮), meatball (⋯), avatar. |
Motion
| Transition | Duration token |
|---|---|
open | motion.duration.fast |
close | motion.duration.instant |
caretRotate | motion.duration.fast |
Responsive behaviour
| Breakpoint | Change |
|---|---|
breakpoint.sm | At and below, the menu may degrade to a bottom-anchored Drawer pattern — full-width, action-list display, explicit close button. The trigger remains; the floating menu becomes a sheet. Especially for overflow menus on mobile where the small-floating-popup pattern works poorly with thumb-targeting. |
breakpoint.md | Above this width, the floating menu renders as authored. Side and align properties honoured; auto-flip and shift work as designed. |
Internationalisation
RTL · mirroring
Side property is direction-neutral (top/right/bottom/left are physical). Align is logical. Caret moves from inline-end of the trigger (visual right in LTR) to inline-end (visual left in RTL) via logical positioning. Caret rotation is direction-neutral. Menu alignment follows trigger inline-start in both directions. Submenu opens via ArrowRight in LTR / ArrowLeft in RTL — keyboard model follows logical inline direction.
Text expansion
Menu item labels grow with translation; menu inline-size matches longest item by canon. Long-text languages (German, Russian) may exceed common menu widths — design at sm size with care. Trigger label follows Button's expansion rules. Keyboard shortcut indicators (e.g. "⌘K", "Ctrl+K") at the inline-end of menu items are direction-neutral but their position mirrors with the menu inline direction.
Variants, properties, states
Variants
Structurally different versions of the component.
standard icon-only Properties
The same component, parameterised.
| Property | Type |
|---|---|
hasCaret | boolean |
side | top | right | bottom | left |
align | start | center | end |
size | sm | md | lg |
States
Browser/user-driven (interactive) vs. app-driven (data).
| Kind | States |
|---|---|
interactive | hoverfocus-visibleactivedisabled |
data | closedopeningopenclosing |
State transitions
| From | To | Trigger |
|---|---|---|
closed | opening | User activates the trigger (Enter / Space / ArrowDown / ArrowUp / click). `aria-expanded` flips to true; the menu mounts; focus moves into the menu landing on first item (or last item for ArrowUp activation per APG). |
opening | open | The enter animation completes (or, under prefers-reduced-motion reduce, immediately). Menu is ready for keyboard navigation; ArrowDown / ArrowUp move between items. |
open | closing | User selects a menuitem (Enter / Space activates the item and closes the menu by canon); presses Escape; tabs out of the menu (Tab / Shift+Tab close); clicks outside the menu; activates a menuitemcheckbox (toggle without close per consumer choice). |
closing | closed | The exit animation completes (or immediately under reduced motion). Focus restores to the trigger. `aria-expanded` flips to false; menu unmounts. |
Figma↔Code mismatches
- 01 Figma
A "menu" drawn as a Popover with arbitrary content
CodeA `role="menu"` with strict APG menu contract (menuitems only, roving tabindex, ArrowKeys navigate)
ConsequenceDesigners may treat menu and popover as interchangeable — both are floating panels triggered by a button. Implementations following the Figma file ship `role="menu"` with form controls or non-menuitem content inside; SR users hear "menu" and expect menuitem-only content with the menu-keyboard contract; the actual content is something else and the contract breaks.
CorrectDistinguish at the canonical level: Menu = list of commands with `role="menu"` and APG menu-keyboard; Popover = arbitrary interactive content with `role="dialog"` or `role="region"` and Tab-into-content. If the floating surface contains form controls, use Popover, not MenuButton.
- 02 Figma
Menu items drawn as anchor-styled (link-coloured underlined text)
CodeMenu items as `<button>` (or appropriate element with `role="menuitem"`)
ConsequenceDesigners compose menus from link-styled items because menus often "feel like navigation". Implementations following the design ship `<a>` with click handlers that perform actions (not navigate); middle-click opens an empty page; copy-link captures `#`. Or developers ship buttons styled as links — visual confusion remains.
CorrectMenu items inside a MenuButton are buttons (perform actions) by canon. Visual styling may match link styling (underlined, accent colour) but the underlying element is a button or a div with `role="menuitem"` plus the keyboard contract. For navigation menus (where each item navigates to a URL), use SidebarNav or a navigation list, not MenuButton.
- 03 Figma
Submenu drawn as a separate menu component, no parent-child relationship
CodeSubmenus via APG menu-button + menu nesting with `aria-haspopup="menu"` on the parent menuitem
ConsequenceDesigners draw a top-level menu and a submenu as separate components. Implementations following the design wire them as independent menus; the parent menuitem has no `aria-haspopup`, the submenu has no parent reference, keyboard ArrowRight does not open the submenu, ArrowLeft does not close it.
CorrectSubmenus are nested menus where the parent menuitem carries `aria-haspopup="menu"` and `aria-expanded`. Right arrow opens (and moves focus into); Left arrow closes (and returns focus to parent item). The canonical reference documents submenus as a recursive composition pattern but ships single-level menus only in Phase 1 (submenu scope deferred to a follow-up).
- 04 Figma
Caret rotation drawn but no menu enter/exit animation
CodeBoth caret rotation AND menu enter/exit animate together
ConsequenceDesigners animate the caret in mocks but the menu appears instantly (Figma cannot easily mock smooth slide/fade transitions for floating surfaces). Developers shipping faithful-to-mock get jumpy menu transitions; SR users get no announcement while focus moves into the menu.
CorrectCaret rotation and menu enter/exit share the same duration token. Reduced-motion fallback is `instant` — both animations skip together. Document the pairing in the motion block.
Contracts
Non-negotiable contracts
APGAPG: Menu Button pattern `role="menu"` hosts a list of commands invoked on activation and uses the APG menu-keyboard contract (ArrowKeys for item navigation, Enter / Space to invoke, Escape / Tab to close). Form controls and arbitrary interactive content do not belong inside `role="menu"`.
Menus containing inputs or arbitrary interactive content violate the role contract — SR users cannot Tab into the items, and keyboard navigation breaks on both sides. Reach for Popover (`role="dialog"`) when the surface needs to host arbitrary content.
Common mistakes
#menubutton-no-aria-haspopup
Trigger missing `aria-haspopup`
The trigger is a styled button with no `aria-haspopup`. SR users hear "button" with no signal that activation opens a menu. Combined with missing `aria-expanded`, the relationship to the menu is invisible.
Trigger always carries `aria-haspopup="menu"` (or `"true"` for legacy SR support — modern canonical is `"menu"`). `aria-expanded` toggles in sync with the open state. `aria-controls` references the menu's id.
#menubutton-tab-traps-in-menu
Tab cycles within the menu instead of closing
Implementation copies the focus-trap pattern from Modal into the menu. Tab cycles between menuitems instead of moving past the menu and closing it. Keyboard users cannot escape the menu without Escape; cyclical Tab feels broken because menus are non-modal.
Tab on an open menu MOVES focus to the next document focusable AND closes the menu (canonical APG). Shift+Tab same in reverse. ArrowDown / ArrowUp navigate within; Tab is the explicit "I'm done with this menu" key.
#menubutton-no-roving-tabindex
Every menuitem has tabindex="0"
Implementation gives every menuitem a `tabindex="0"`. Tab reaches every item one by one; the menu inflates the page's tab order with `n` extra stops. APG canonical is roving tabindex (only the focused item has `tabindex="0"`, others have `tabindex="-1"`).
Only the currently-focused menuitem has `tabindex="0"`; all others have `tabindex="-1"`. ArrowKeys move the `tabindex="0"` reference along with focus. Tab moves out of the menu (one stop, not n).
#menubutton-arrowdown-doesnt-open
ArrowDown on trigger does not open the menu
The trigger opens the menu only on click / Enter / Space. APG canonical behaviour requires ArrowDown / ArrowUp to also open (and ArrowUp to land focus on the last item). Keyboard users expecting the canonical behaviour cannot navigate efficiently.
Bind ArrowDown and ArrowUp on the trigger: ArrowDown opens the menu and focuses the first item; ArrowUp opens and focuses the last item. Click / Enter / Space focus the first item by canon (some implementations focus none and require a subsequent ArrowKey; APG canonical is to focus first).
#menubutton-no-typeahead
Typeahead by first-letter not implemented
User types a letter expecting to jump to the next menuitem starting with that letter (a canonical APG behaviour). Nothing happens; users with long menus arrow-key through every entry.
Implement first-letter typeahead on the open menu: typing matches the first menuitem starting with that letter and moves focus there. Sequential same-letter cycles through matches. Multi-character timeout (~500ms) accumulates the buffer.
Accessibility hints
| Slot | Accessibility hint | |
|---|---|---|
trigger | Real `<button>` carrying `aria-haspopup="menu"` (or "true" for legacy support; `"menu"` is the modern APG canonical value). `aria-expanded` reflects open state; `aria-controls` references the menu container's id. Icon-only triggers (overflow menus) require an `aria-label` ("More actions", "User menu") because the icon alone is not a label. | |
caret | Decorative — `aria-hidden="true"`. Open state is communicated by `aria-expanded`; the caret visualises it. | |
menu | Apply `role="menu"` and an `id` referenced by the trigger's `aria-controls`. Menu receives DOM focus when opened (unlike Popover where focus stays on trigger). Roving tabindex within the menu — only the currently-focused item has `tabindex="0"`, others have `tabindex="-1"`. | |
menu-item | `role="menuitem"` for plain commands; menuitemcheckbox plus `aria-checked` for toggleables (Bold, Italic); menuitemradio plus `aria-checked` and grouping for mutually-exclusive (text alignment). Disabled items carry `aria-disabled="true"` and stay focusable so SR users hear them; activation is suppressed. | |
separator | `role="separator"` (or no role and `aria-hidden="true"` for purely decorative dividers — APG accepts both). Separators do not receive focus; keyboard navigation skips them. |