# Overflow Menu An overflow menu tucks secondary or contextual actions behind an icon-only \"more\" button. Section: components Docs: https://nebula.kaluza.com/components/overflow-menu --- ## Overview ### When to use - To offer secondary or contextual actions that don't fit the primary UI. The most common place is a row in a [Data Table](/components/data-table/), but it also suits a [Card](/components/card/). - When there are several actions for the same item and showing each as its own button would crowd the layout. ### When not to use - For a single action - show it directly as a standalone [icon-only CTA Button](/components/cta-button/) instead of hiding it behind a menu. - For the primary action on a page or in a form. Keep it visible. - To choose a value. This is a menu of actions, not a replacement for [Select Field](/components/select-field/) or [Combobox Field](/components/combobox-field/). ### Anatomy 1. **Trigger**: an icon-only [CTA Button](/components/cta-button/) showing the vertical ellipsis (kebab) icon. Hovering or focusing it shows a tooltip reading "More actions". 2. **Menu**: the list of actions, attached directly below the trigger. It is 300px wide, and grows up to 350px to fit longer labels. Each item has a label that describes the action that will be performed. 3. **Icon**: an optional icon before the label. ## Basic usage ```tsx alert('Edit') }, { label: 'Download statement', icon: 'download', onClick: () => alert('Download statement') }, { label: 'View in new window', icon: 'new-window', onClick: () => alert('View in new window') }, { label: 'Archive', onClick: () => alert('Archive') }, ]} /> ``` Each item has a `label`, and optionally an `icon`, `destructive`, and one of three kinds: a button with an `onClick` handler, a link with an `href`, or a router link with `as` and `to`. See the Properties tab for the full list. ### Actions and links Use `onClick` for something that happens on the page, such as archiving or deleting. Use `href` for an item that goes somewhere else. The two look the same, but a link is a real anchor, so it can be opened in a new tab, and assistive technology announces it as a link rather than an action. ```tsx alert('Downloading') }, { label: 'View account', icon: 'link', href: '/account' }, ]} /> ``` ## Variants ### Tertiary and secondary The trigger is built on the icon-only CTA Button, so it is only available as **tertiary** (the default) or **secondary**. Tertiary suits dense layouts such as table rows. Use secondary when the trigger needs more visual grounding and has to be clearly separated from surrounding text. ```tsx alert('Edit') }, { label: 'Delete', icon: 'delete', destructive: true, onClick: () => alert('Delete') }, ]} /> ``` ### Small size Set `size="small"` for a more compact trigger. The same guidance as the [small CTA Button](/components/cta-button/) applies. The menu is the same size either way. ```tsx alert('Edit') }, { label: 'Delete', icon: 'delete', destructive: true, onClick: () => alert('Delete') }, ]} /> ``` ### Without icons Icons are optional. When no item has one, labels sit at the left edge of the menu with no indent. When only some items have one, the others are indented so every label lines up. ```tsx alert('Mark as paid') }, { label: 'Archive', onClick: () => alert('Archive') }, { label: 'Add to group', onClick: () => alert('Add to group') }, { label: 'Report a problem', onClick: () => alert('Report a problem') }, ]} /> ``` ### Destructive item Set `destructive` on an item that deletes or removes something. Its label and icon use the error colour. Place it last. ```tsx alert('Edit') }, { label: 'Archive', onClick: () => alert('Archive') }, { label: 'Delete', icon: 'delete', destructive: true, onClick: () => alert('Delete') }, ]} /> ``` ### Links Give an item an `href` to make it a link instead of a button, for example to take the user to another page. It sits in the menu and is operated like every other item, and assistive technology announces it as a link. Set `opensInNewWindow` for a link that opens a new tab or window. A link can also have an `onClick`, which runs before the link is followed, for example to send an analytics event. It receives the click event, so it can call `preventDefault()` to stop the link being followed. To use a client-side router, pass the router's link component as `as` and the destination as `to`, as you would for a [Text Link](/components/text-link/). For example, with React Router: `{ label: 'View account', as: Link, to: '/account' }`. ```tsx alert('Archive') }, ]} /> ``` ## Placement The menu lines up with the left edge of the trigger. If that would run it off the right of the screen, for example for a trigger in the last column of a wide table, it lines up with the right edge of the trigger instead. There is nothing to configure. ## In context ### In a table Put the trigger in the last column of each row. Give the column a visually hidden heading, and give each menu a label that names its row. | Account | Balance | Actions | | --- | --- | --- | | Electricity | £42.10 | null | | Gas | £18.75 | null | _Accounts_ ### In a card ```tsx Direct debit alert('Change amount') }, { label: 'Change date', onClick: () => alert('Change date') }, { label: 'Cancel direct debit', icon: 'delete', destructive: true, onClick: () => alert('Cancel direct debit') }, ]} />

£50.00 on the 1st of every month.

``` ## Rules and guidance ### Usage - **Overflow menu** SHOULD NOT be used for a single action. Rationale: With one action there is nothing to collapse. Show it directly as a standalone icon-only CTA Button instead of hiding it behind a menu. [id: overflowmenu.no-single-action; testability: context] - **Overflow menu** SHOULD contain roughly two to six actions. Rationale: Above six the menu becomes hard to scan. Reconsider the set of actions, for example by grouping or restructuring it, before reaching for this component. This is guidance, not a hard limit. [id: overflowmenu.action-count; testability: manual] - **Overflow menu** MUST NOT hold the primary action on a page or in a form. Rationale: The primary action should stay visible. Actions inside a menu are less discoverable. [id: overflowmenu.no-primary-action; testability: context] - **Overflow menu** MUST NOT be used to choose a value. Rationale: It is a menu of actions. Use Select Field or Combobox Field to choose a value. [id: overflowmenu.no-value-selection; testability: context] - **Overflow menu trigger** SHOULD be tertiary. Rationale: Tertiary is the quietest option and suits dense layouts such as table rows. Exception: the trigger needs more visual grounding and has to be clearly separated from surrounding text, in which case use secondary. [id: overflowmenu.tertiary-default; testability: manual] ### Placement - If the menu belongs to a row in a table: **Overflow menu trigger** SHOULD be placed in the last column of the row. Rationale: A consistent position lets people find the actions for every row in the same place. [id: overflowmenu.table-last-column; testability: manual] - **Destructive items** MUST be placed last in the menu. Rationale: Keeping destructive actions apart from the common ones reduces accidental selection. [id: overflowmenu.destructive-last; testability: manual] ### Content - **Item labels** MUST use sentence case, for example "Download statement" and not "Download Statement". [id: overflowmenu.sentence-case; testability: manual] - **Item labels** MUST NOT end with punctuation. [id: overflowmenu.no-trailing-punctuation; testability: manual] - **Item labels** SHOULD start with a verb and be short. Rationale: A label that names the action is quick to scan. [id: overflowmenu.verb-first; testability: manual] - **The most common action** SHOULD come first. [id: overflowmenu.common-action-first; testability: context] - **Item icons** SHOULD only be used where they help people identify the action. Rationale: An icon that doesn't aid identification is decoration. Don't add one to every item for the sake of consistency. [id: overflowmenu.icons-identify; testability: manual] - **Every item** MUST have a text label, with any icon in addition to it. Rationale: The icon is hidden from assistive technology and can be misread, so the label carries the meaning. [id: overflowmenu.icons-and-text; testability: auto] ### Interaction - **Destructive items** MAY ask for confirmation, for example in a modal. Rationale: Whether to confirm depends on how severe or hard to reverse the action is. It isn't mandatory, because choosing a destructive item already takes two clicks. [id: overflowmenu.destructive-confirmation; testability: context] ## Accessibility ### In brief Overflow Menu is a button that opens a menu of actions. The trigger is named by its label and reports whether the menu is open, and each item is a menu item named by its label. ### What Nebula provides - A trigger button named by its `label`, with `aria-haspopup="menu"` and an `aria-expanded` state. - A `menu` named by the same label, whose items have the `menuitem` role. The icons are hidden from assistive technology. - Focus moves to the first item when the menu opens, and returns to the trigger when it closes. - Link items keep the `menuitem` role and are given a role description of "link". Links that open a new window add "(opens in new window)" after the label. ### Keyboard behaviour The trigger is reached with `Tab` and opens the menu from the keyboard. The arrow keys, `Home`, `End` and typing move between the items, `Esc` closes the menu and `Tab` moves on. #### Focus order 1. The trigger button. 2. The active menu item, while the menu is open. Only one item is a focus stop at a time, and the arrow keys move between them. #### Interactions - **Enter or Space** - Opens the menu on the trigger, and chooses the focused item - **Up** - Opens the menu on its last item, from the trigger - **Up or Down** - Moves between the items, wrapping at either end - **Home or End** - Moves to the first or last item - **A letter** - Jumps to the first item that starts with it - **Esc** - Closes the menu and returns focus to the trigger - **Tab** - Closes the menu and moves focus on ### Accessible design #### Give each menu its own label When a page has several overflow menus, such as one per table row, a screen reader user hears "More actions" repeatedly. Name what each menu acts on, for example "More actions for Electricity". WCAG: [2.4.6 Headings and Labels](https://www.w3.org/WAI/WCAG22/Understanding/headings-and-labels.html) [2.5.3 Label in Name](https://www.w3.org/WAI/WCAG22/Understanding/label-in-name.html) #### Don't make the menu the only way to reach a critical action Actions inside a menu are harder to find, and a screen reader user has to open the menu to learn they exist. If the task depends on an action, show it as a visible button as well. #### Name the action and what it acts on in a destructive item Red is the only other signal, and not everyone sees it. Write "Close account", not "Close". WCAG: [1.4.1 Use of Color](https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html) ### Accessible development #### Pass a `label` to every menu when there is more than one on the page The default "More actions" is the same for every menu, so a screen reader user cannot tell them apart. The label is also the trigger's tooltip. WCAG: [2.4.6 Headings and Labels](https://www.w3.org/WAI/WCAG22/Understanding/headings-and-labels.html) #### If an item opens a modal or moves focus, do it in `onClick` `onClick` runs as the menu closes, and focus then returns to the trigger unless something else, such as a modal, takes it. A modal opened from `onClick` takes focus as it opens, and returns it to the trigger when it closes. WCAG: [2.4.3 Focus Order](https://www.w3.org/WAI/WCAG22/Understanding/focus-order.html) #### Use `href` for an item that goes to another page, and `onClick` for an action An `href` gives a real link that can be opened in a new tab and is announced as a link. A button that navigates is announced as an action. WCAG: [4.1.2 Name, Role, Value](https://www.w3.org/WAI/WCAG22/Understanding/name-role-value.html) ### Accessibility testing status Reviewed on 13 October 2026 against v17.0.1. | Test | Status | Tested with | | --- | --- | --- | | Default state | Tested | axe-core via the Storybook test runner (CI) | | Advanced states | Manually tested | axe-core via the Storybook test runner (CI), Chrome on macOS, axe DevTools extension | | Keyboard navigation | Manually tested | Chrome on macOS | | Screen reader | Manually tested | VoiceOver with Chrome on macOS | What each test covers: - Default state: The component as it first appears, before anything is clicked or changed. - Advanced states: Every other state and variant: error, open, selected, each size and style, and hovering. - Keyboard navigation: Whether every function can be reached and used with the keyboard alone, with a visible focus ring. - Screen reader: Whether a screen reader announces what is on screen correctly: the name, the role and any state. What these statuses mean: - Tested: Every check in this row passed in an automated run that repeats on every pull request. - Manually tested: Every check in this row was run once, by a person or with a tool they started, and all of them passed. It is not re-run on every change. - Partially tested: Some checks in this row were run once and passed; the rest are still to do. - Issue found: A check in this row failed. The ticket is shown beside the status. - Not tested: No check in this row has been run yet. - Not applicable: This row does not apply to the component, for the reason given beside it. The table records which checks were run on this component on its own. It is not a statement of WCAG conformance, and it does not cover how the component is used. ## Properties ### OverflowMenu Also accepts all props from `ButtonHTMLAttributes`, `HTMLAttributes`. - `label` (type `string`, optional, default `'More actions'`). The accessible name of the icon-only trigger, which is also shown as its tooltip. Give each menu a label that says what it acts on, for example "More actions for Electricity", when several appear on one page. - `items` (type `OverflowMenuItem[]`, required). The actions in the menu, in the order they are shown. Designed for roughly two to six; for a single action use an icon-only CTA Button instead. An item is a button with an `onClick`, a link with an `href`, or a router link with `as` and `to`. - `variant` (type `'secondary' | 'tertiary'`, optional, default `'tertiary'`). The style of the trigger button. The icon-only button is not available as primary or destructive. Use `secondary` when the trigger needs visual grounding and must be clearly separated from surrounding text. - `size` (type `'default' | 'small'`, optional, default `'default'`). The size of the trigger button. The menu is the same size either way. - `defaultOpen` (type `boolean`, optional, default `false`). Opens the menu on first render. Intended for visual regression screenshots and stories that need to show the menu. - `as` (type `ElementType`, optional). Renders the component as a different element or component, e.g. `as="section"`. - `forwardedAs` (type `ElementType`, optional). Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. ## Styles Reference only; you do not need this to use the component. This is the CSS its styled elements use, with theme tokens shown by name. `${...}` is an interpolation (a theme token, a mixin or another value from the code). A `&[prop]` or `@when ` block holds CSS that applies only under that condition; these are notation, not valid CSS. ### menu The popover surface that holds the items, attached below the trigger. ```css box-sizing: border-box; width: max-content; min-width: 30rem; max-width: min(35rem, calc(100vw - ${space[4]})); padding: ${space[2]}; overflow-y: auto; background: ${colors.canvas}; border: ${borderWidths.standard} solid ${colors.borderMuted}; border-radius: ${radii.standard}; box-shadow: ${shadows.standard}; &:focus { outline: none; } ``` ### item One action in the menu, with its icon slot and label. Link items render this as an anchor. ```css position: relative; display: flex; align-items: center; gap: calc(${space[2]} + 0.2rem); box-sizing: border-box; width: 100%; min-height: ${space[7]}; padding: 0 ${space[2]}; border: 0; border-radius: ${radii.standard}; background: transparent; color: ${$destructive ? colors.error : colors.body}; font-family: ${fonts.body}; font-weight: ${fontWeights.normal}; font-size: ${responsiveFontSizes.body[0]}; line-height: ${lineHeights.body}; text-align: left; text-decoration: none; cursor: pointer; transition: ${transitions.standard}; ${mediaQueries.smallAndUp} { font-size: ${responsiveFontSizes.body[1]}; } path { fill: currentColor; } &:hover, &:focus { background: ${$destructive ? colors.errorMuted : colors.primaryMuted}; } &:focus-visible { outline: none; box-shadow: inset 0 0 0 ${borderWidths.heavy} ${colors.focus}; } ``` ### item-label The text of an item, which takes up the space beside the icon. ```css flex: 1 1 auto; ``` ### item-icon The optional icon shown before an item's label. ```css flex: 0 0 auto; ``` ### item-icon-spacer Empty space the size of an icon, which keeps labels aligned when only some items have an icon. ```css flex: 0 0 auto; width: ${'1.6rem'}; height: ${'1.6rem'}; ```