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.

Highlight
Fig 1.1 · Menu Button · Designer view
Designer

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
Designer

Token usage per slot

trigger
spacing
  • paddingspacing.compact
  • gapspacing.compact
radius
  • cornerradius.md
color
  • ringcolor.border.focus
caret
color
  • foregroundcolor.text.muted
menu
spacing
  • paddingspacing.tight
radius
  • cornerradius.md
color
  • backgroundcolor.surface.raised
  • bordercolor.border.subtle
elevation
  • shadowelevation.lg
menu-item
spacing
  • paddingspacing.compact
  • gapspacing.compact
radius
  • cornerradius.sm
color
  • foregroundcolor.text.primary
typography
  • sizetext.sm
separator
color
  • backgroundcolor.border.subtle
Both

Figma ↔ Code property map

FigmaKindCodeNotes
VariantEnumvariantMaps standard / icon-only.
SideEnumsidetop / right / bottom / left. Authored placement preference; auto-flips on viewport collision.
AlignEnumalignstart / center / end along the perpendicular axis.
SizeEnumsizesm / md / lg. Affects trigger and menu typography.
Has CaretBooleanhasCaretToggles the caret slot. Default true for standard variant; default false for icon-only.
Has SeparatorsBooleanseparatorsToggles whether menu groups are visually separated. Decorative — does not change keyboard behaviour.
Item CountEnumitems.lengthFigma 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 LabelTexttriggerLabelFor standard variant. Icon-only variant uses aria-label instead.
Trigger IconSlottriggerIconFor icon-only variant. Common: kebab (⋮), meatball (⋯), avatar.
Designer

Motion

TransitionDuration token
openmotion.duration.fast
closemotion.duration.instant
caretRotatemotion.duration.fast
Easing
motion.easing.decelerate
Reduced motion
Instant (jump cut)
Designer

Responsive behaviour

BreakpointChange
breakpoint.smAt 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.mdAbove this width, the floating menu renders as authored. Side and align properties honoured; auto-flip and shift work as designed.
Both

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.

Both

Variants, properties, states

Variants

Structurally different versions of the component.

standard icon-only

Properties

The same component, parameterised.

PropertyType
hasCaret boolean
side top | right | bottom | left
align start | center | end
size sm | md | lg

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
hoverfocus-visibleactivedisabled
data
closedopeningopenclosing
Both

State transitions

FromToTrigger
closedopeningUser 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).
openingopenThe enter animation completes (or, under prefers-reduced-motion reduce, immediately). Menu is ready for keyboard navigation; ArrowDown / ArrowUp move between items.
openclosingUser 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).
closingclosedThe exit animation completes (or immediately under reduced motion). Focus restores to the trigger. `aria-expanded` flips to false; menu unmounts.
Both

Figma↔Code mismatches

  1. 01
    Figma

    A "menu" drawn as a Popover with arbitrary content

    Code

    A `role="menu"` with strict APG menu contract (menuitems only, roving tabindex, ArrowKeys navigate)

    Consequence

    Designers 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.

    Correct

    Distinguish 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.

  2. 02
    Figma

    Menu items drawn as anchor-styled (link-coloured underlined text)

    Code

    Menu items as `<button>` (or appropriate element with `role="menuitem"`)

    Consequence

    Designers 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.

    Correct

    Menu 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.

  3. 03
    Figma

    Submenu drawn as a separate menu component, no parent-child relationship

    Code

    Submenus via APG menu-button + menu nesting with `aria-haspopup="menu"` on the parent menuitem

    Consequence

    Designers 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.

    Correct

    Submenus 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).

  4. 04
    Figma

    Caret rotation drawn but no menu enter/exit animation

    Code

    Both caret rotation AND menu enter/exit animate together

    Consequence

    Designers 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.

    Correct

    Caret 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.

Both

Contracts

Non-negotiable contracts

  1. 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.

Designer

Common mistakes

Blocker

#menubutton-no-aria-haspopup

Trigger missing `aria-haspopup`

Problem

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.

Fix

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.

Blocker

#menubutton-tab-traps-in-menu

Tab cycles within the menu instead of closing

Problem

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.

Fix

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.

Blocker

#menubutton-no-roving-tabindex

Every menuitem has tabindex="0"

Problem

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"`).

Fix

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).

Major

#menubutton-arrowdown-doesnt-open

ArrowDown on trigger does not open the menu

Problem

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.

Fix

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).

Minor

#menubutton-no-typeahead

Typeahead by first-letter not implemented

Problem

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.

Fix

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.