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.

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.

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

FigmaTypeCodeNotes
VariantVariantvariantMaps standard / icon-only.
SideVariantsidetop / right / bottom / left. Authored placement preference; auto-flips on viewport collision.
AlignVariantalignstart / center / end along the perpendicular axis.
SizeVariantsizesm / 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 CountVariantitems.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 IconInstance SwaptriggerIconFor 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.

standardicon-only

Properties

The same component, parameterised.

PropertyType
hasCaretboolean
sidetop | right | bottom | left
alignstart | center | end
sizesm | 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.

Designer

Common mistakes

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

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

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

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

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

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.