SplitButton
Primary action button paired with a dropdown toggle for related secondary actions, with collision-aware menu placement and optional nested submenus.
Status: beta. Group: actions. Markdown version: /design-system/components/split-button.md
When to use
SplitButton pairs one dominant action with a menu of close variants of that same action: Save with Save as / Save a copy, Export with format choices. Both segments share Button's weight, size, and surface so the pair reads as a single control.
When not to use
- Do not use it for general-purpose dropdown menus without a dominant default action.
- Avoid mixing destructive options into a constructive menu.
- Do not use it for more than one submenu level.
- Do not use SplitButton as a general dropdown menu; if there is no dominant default action, use a plain menu trigger instead.
Look-alikes
- ButtonGroup: Do not put a menu trigger inside; SplitButton owns primary-action + menu.
Props
| Prop | Type | Default | Description | Required |
|---|---|---|---|---|
label | React.ReactNode | none | Main label rendered on the primary segment. | Yes |
onPrimaryClick | React.MouseEventHandler<HTMLButtonElement> | none | Primary action invoked when clicking the main segment. | No |
options | SplitButtonOption[] | none | List of alternative actions shown in the dropdown. | Yes |
menuAlign | "end" | "start" | start | Alignment of the dropdown menu along the toggle edge. | No |
disabled | boolean | false | Disable both segments. | No |
toggleLabel | string | none | Accessible label for the dropdown trigger. | No |
className | string | none | Optional className forwarded to the outer wrapper. | No |
Variants
- Available variants:
primary,secondary,tertiary(defaultprimary). - Available surfaces:
default,onDark,onBrand(defaultdefault). - Available sizes:
sm,md,lg(defaultmd). - Allowed
menuAlignvalues:end,start(defaultstart).
Invalid combinations
The contract records no invalid combinations.
Keyboard
| Key | Behaviour |
|---|---|
| Tab | Both segments are focusable in sequence. |
| Enter / Space | On the primary segment; fires onPrimaryClick. |
| Enter / Space / ArrowDown | On the toggle; opens the menu and focuses the first enabled item. |
| ArrowDown / ArrowUp | Move through enabled items (roving tabindex). |
| Home / End | Jump to first / last enabled item. |
| ArrowRight | Opens a submenu; ArrowLeft closes it. |
| Escape | Closes the menu and returns focus to the toggle. |
| Tab | Inside the open menu is trapped and cycles items until Escape. |
Rules
Every item under When not to use is a MUST NOT. MUST and SHOULD follow RFC 2119.
| Level | Rule | How it is checked |
|---|---|---|
| MUST | ARIA: Toggle carries aria-haspopup='menu', aria-expanded, aria-controls, and an accessible name (toggleLabel, default t('splitButton.moreOptions')). | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Menu uses role='menu' with role='menuitem' buttons; submenu triggers add aria-haspopup and aria-expanded. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Option icons are decorative (aria-hidden); the option label is the announcement. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | Required stories produce no axe violations at the configured severity. | Automated by npm run test:stories:matrix:ci. WCAG 2.5.8. |
| MUST | The accessibility tree (role, name, state) matches the committed snapshot for every required story. | Automated by npm run test:stories:matrix:ci. WCAG 1.3.1, 4.1.2. |
| MUST | The component remains legible and operable under a real browser forced-colors: active pass. | Automated by npm run test:stories:hc. |
| MUST | Every documented keyboard interaction works as specified. | Not yet verified: no evidence record. WCAG 2.1.1, 2.1.2. |
| MUST | A human completed an end-to-end accessibility review of this component. | Manual review, last on 2026-07-03. |
| SHOULD | Make the primary segment the safest, most common choice; the menu holds variants of it, not unrelated commands. | Not stated in the contract. |
| SHOULD | Keep options parallel in wording (Save as, Save a copy, Save to cloud) so the menu scans as one family. | Not stated in the contract. |
| SHOULD | Give every option an id; labels can be translated, ids anchor analytics and tests. | Not stated in the contract. |
| SHOULD | The dropdown always portals (shared Menu primitive), so it never clips even inside an overflow-hidden container. | Not stated in the contract. |
| SHOULD | Provide toggleLabel when the menu is not generic more options, for example Choose export format. | Not stated in the contract. |
| SHOULD NOT | Do not mix destructive options into a constructive menu; Delete does not belong under Save. | Not stated in the contract. |
| SHOULD NOT | Do not nest submenus more than one level; the component supports exactly one level by design. | Not stated in the contract. |
Install and import
- Install:
npm install @digitaltableteur/react(version 0.9.1, restricted access on npm). - Import:
import { SplitButton } from "@digitaltableteur/react"; - Entry point:
@digitaltableteur/react/actionsexports it too. - Styles: import
@digitaltableteur/react/style.cssonce at the app boundary. - Also available as the custom element
<dt-split-button>; see the custom elements manifest.
Links
- Machine-readable contract (Design System Contract 1.0)
- Markdown version of this page
- Component index as JSON
- All components
- Source: The implementation lives in a private repository. This page, its Markdown twin and the contract are generated from it at build time and are the public source of truth.