# 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
- Page: https://www.digitaltableteur.com/design-system/components/split-button
- Contract: https://www.digitaltableteur.com/contracts/v1/SplitButton.contract.json

## 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` (default `primary`).
- Available surfaces: `default`, `onDark`, `onBrand` (default `default`).
- Available sizes: `sm`, `md`, `lg` (default `md`).
- Allowed `menuAlign` values: `end`, `start` (default `start`).

## 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/actions` exports it too.
- Styles: import `@digitaltableteur/react/style.css` once 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): https://www.digitaltableteur.com/contracts/v1/SplitButton.contract.json
- Markdown version of this page: https://www.digitaltableteur.com/design-system/components/split-button.md
- Component index as JSON: https://www.digitaltableteur.com/design-system/components.json
- All components: https://www.digitaltableteur.com/design-system/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.
