action menu
An action menu keeps several actions behind one small button, so a table row or a toolbar stays calm until someone needs one of them. The menu opens above the page, so nothing around it can cut it off.
when to use
- For the actions on one thing, such as one row of a table: “pick a release”, “grab now”. One menu per row keeps a table calm.
- As the overflow menu of a toolbar, for the actions that don't fit. The toolbar does this on its own, behind “more actions”.
- For a few related commands behind a button with words on it: “export”, then “as csv” or “as json”.
when to use something else
- For the one emphasized action of a view. Show it as a button. Hidden in a menu, nothing reads as the next step.
- For actions on several selected rows. Use a selection toolbar, which says how many rows it acts on.
- To choose a value, such as a sort order or a filter. A menu runs commands, and it doesn't show the current choice. Use a select, or a multi select for several values.
- To go to another page. Use a link. A menu item is a button, so it can't be opened in a new tab.
- To ask for a decision. Use a modal.
anatomy
- Trigger. The button that opens the menu. Usually it is a small, quiet icon button with three little squares in a row. With words on it instead, it is a standard button with your text and a small downward arrow after it.
- Menu. A panel that floats above everything else on the page, with a thin frame, square corners and a soft shadow. It is never narrower than its trigger, and never wider than the window.
- Item. One action per line, in monospace and lowercase. The item under the pointer, or the one with focus, turns lighter. With keyboard focus, it also gets a bar in the focus colour on its left edge.
- Item icon (optional). A small icon before the label, as decoration.
variants and states
The trigger is an icon, or words. Use the icon on a row, where the row already says what the menu acts on. Use words where the trigger stands on its own, such as “export”.
The trigger comes in two of the heights every control shares. The small one is the usual choice, for table rows and toolbars; the medium one goes next to other medium controls.
An item can be disabled: it is dimmed, it can't be run, and the arrow keys skip it. An item can also be a danger item, for an action that deletes something or can't be undone. Its label is amber, and it stays amber under the pointer and with focus. Put danger items last.
A busy menu shows a spinner in its trigger, and it can't open. A disabled menu is off as a whole. Either way, the trigger can't take focus, so the keyboard passes over it.
Usually the menu lines up with the right edge of its trigger. It can line up with the left edge instead.
behaviour
opening and choosing
- A click on the trigger opens the menu, and another click closes it. While the menu is open, it keeps the focus.
- From the keyboard, enter, space or the down arrow key opens the menu on its first item. The up arrow key opens it on its last item.
- In the menu, the up and down arrow keys move from item to item, and go round at the ends. home and end jump to the first and the last item.
- A click, enter or space chooses an item. The menu closes and focus goes back to the trigger before the action runs. So when the item opens a modal, focus comes back to the trigger once the dialog closes.
- A click on a disabled item does nothing, and the menu stays open.
closing
- esc closes the menu and puts focus back on the trigger. In a modal, it closes only the menu; the dialog stays open.
- tab closes the menu too, and focus moves on from the trigger, as if the menu had not been there.
- A press anywhere outside the menu and its trigger closes the menu. Focus stays where that press put it.
where it opens
- The menu opens just below its trigger. When it wouldn't fit below and there is room above, it opens above instead. It always keeps a small margin from the left and right edges of the window.
- No table cell, scrolling area or panel cuts the menu off. While it is open, it follows its trigger when anything scrolls or the window changes size, and it never flashes up in the wrong place first.
The full keyboard map is on the action menu's React page.
accessibility
- Name the thing, not the action. An icon trigger has no words on it, so it needs a name written for it: its accessible name. The browser also shows that name as a small tooltip under the pointer. Name the object the menu acts on: “actions for S04E07”, “actions for worker-2”. A table whose fifty triggers are all called “more” gives a screen reader user fifty buttons they can't tell apart.
- The menu takes its name from its trigger.
- With words on the trigger, those words are its name. What people see is what speech input users say to press it.
- Danger is shown by colour only. So the label has to say what happens, such as “delete worker”. For what can't be undone, ask in a modal once the item is chosen.
- Item icons are hidden from assistive technology.
Today:
- The arrow keys skip disabled items, so people who use a keyboard never land on one. They may not learn that it exists, or why it is off. Say the reason on the page, in the row's status.
- Typing a letter doesn't move to the item that starts with it.
- A menu has no separators and no groups.
- There are no styles for forced colours, the high-contrast setting some operating systems offer. A focused item shows only a fill and a bar, and forced colours remove both.
content
- Items. A verb and its object, in lowercase: “pick a release”, “grab now”, “open logs”, “delete worker”.
- Order. The most used first, and danger last.
- The same menu on every row. In a list, disable an item that doesn't apply to a row rather than leaving it out, and let the row's status say why.
- The icon trigger's name. “actions for” and the object's name as the row shows it: “actions for S04E07”.
- Words on a trigger. One or two words for what is behind it: “export”.
do and don't
related
- Toolbar: its overflow is an action menu, and its selection toolbar holds the actions for several rows.
- Selection and bulk actions: one menu per row, and the actions for a selection.
- Icon button: the trigger, and why a button with only an icon needs a name.
- Modal: to confirm a danger item before it runs.
specs
| part | spec |
|---|---|
| Trigger | By default a ghost IconButton with three 2.5px squares (icon replaces them): 28px square at size="sm", 34px at size="md". With buttonLabel, a default Button with your text and a trailing 12px chevron. |
| Menu | Rendered into document.body (a React portal) only while open, position: fixed, z-index: 150, so no table cell, scroll container or panel that hides its overflow clips it. --surface fill, a 1px --border-2 frame, border-radius: 0, padding 4px 0, a shadow of 0 8px 24px black at 45%. At least 160px wide, or as wide as the trigger if that is wider; at most the viewport's width minus 16px. |
| Item | At least 32px tall, padding 4px 12px, JetBrains Mono 13px in --fg-2, lowercased with text-transform, on one line. Under the pointer and on focus: a --surface-2 fill and a --fg label. Keyboard focus adds a 2px --focus-ring bar inside the left edge. |
| Item icon | icon on an item: decoration 8px before the label, in --fg-muted, hidden from assistive technology. |
| Disabled item | disabled on an item: 45% opacity and a not-allowed cursor. It stays in the menu, can't be run, and the arrow keys skip it. |
| Danger item | danger on an item: the label in --signal-amber at rest, on hover and on focus. |
| Placement | 4px under the trigger. It opens above instead when it would come within 8px of the viewport's bottom edge and there is room above. It keeps 8px from the viewport's left and right edges, is measured before it shows, and follows the trigger on any scroll and on resize. |
| setting | in code | what it does |
|---|---|---|
| Name | label |
The icon trigger's accessible name, also its tooltip: “actions for S04E07”. The menu is named by its trigger. |
| Words on the trigger | buttonLabel |
A text trigger. Its visible text is its name (WCAG 2.5.3, label in name), and label isn't used. |
| Size | size="sm" (the default), size="md" |
The trigger's height: 28px or 34px. |
| Alignment | align="end" (the default), align="start" |
The menu lines up its end edge with the trigger's, or its start edge. |
| Busy | loading |
A spinner in the trigger, and the menu can't open. The trigger is disabled, so it can't be focused. |
| Disabled | disabled |
The whole menu is off. The trigger is disabled, so it can't be focused. |