# Button

> Primary action control with an orthogonal variant (visual weight) and tone (semantic color) model, loading state, and polymorphic button/link rendering.

- Status: stable
- Group: actions
- Page: https://www.digitaltableteur.com/design-system/components/button
- Contract: https://www.digitaltableteur.com/contracts/v1/Button.contract.json

## When to use

Button triggers an action: submitting a form, confirming a choice, or starting a flow. It renders a real button, or an anchor when href is given, and carries the full variant (visual weight) by tone (semantic color) matrix.

## When not to use

- Do not put a tertiary Button on a dark or brand band without surface="onDark" or surface="onBrand"; the default tertiary colours assume a light surface.
- Do not pair an icon-only button with a tooltip as the *only* accessible name; the tooltip becomes the aria-label, but its visible text never shows on touch or keyboard focus.
- Do not nest a Button inside another interactive control. Buttons are terminal.
- Do not use a Button for inline navigation in body copy. Use **Link**.
- Do not use Button for pure navigation; if it only takes the user to another page, use Link (Button with href is for action-shaped navigation like Get started).
- Do not use Button for an on/off state; use Switch. For multi-select within a group, use Checkbox or CheckboxGroup.

## Look-alikes

- Avatar: Do not nest an Avatar inside a Button. Pick one; Avatar already owns the click affordance when clickable, and Button owns it when not.
- Badge: Do not use Badge as an interactive action or toggle; use Button for actions and Switch or Checkbox for state.
- Checkbox: Do not use Button for an on/off state; use Switch. For multi-select within a group, use Checkbox or CheckboxGroup.
- CheckboxGroup: Do not use Button for an on/off state; use Switch. For multi-select within a group, use Checkbox or CheckboxGroup.
- Icon: Do not build icon-only buttons from a bare Icon; use Button with icon and accessibleName so focus, hit target and keyboard activation are owned by the right component.
- IconButton: Do not use IconButton for primary marketing CTAs; text plus icon is more discoverable, use Button with an icon prop.
- Link: Do not use Button for pure navigation; if it only takes the user to another page, use Link (Button with href is for action-shaped navigation like Get started).
- SlideButton: Do not use it for form submission or in-page actions; use Button, which renders a real button.
- Switch: Do not use Button for an on/off state; use Switch. For multi-select within a group, use Checkbox or CheckboxGroup.
- Title: Do not nest a Title inside a Button. Use the Button's text slot directly.

## Props

| Prop | Type | Default | Description | Required |
| --- | --- | --- | --- | --- |
| `submits` | `boolean` | none | When true, sets `type="submit"`. | No |
| `clickAction` | `(event: React.MouseEvent<HTMLButtonElement>) => void \| Promise<void>` | none | Async click handler. `onClick` still fires first if both are provided. While the returned promise is pending, the button shows its `loading` state, sets `aria-busy`, and disables itself (deduping repeat clicks). Rejections are swallowed after clearing the pending state; callers own error UX (e.g. surface a toast from within `clickAction` itself). | No |
| `href` | `string` | none | Destination URL. When provided, the button renders as an anchor. | No |
| `target` | `"_self" \| "_blank" \| "_parent" \| "_top"` | none | No description in the contract. | No |
| `rel` | `string \| undefined` | none | No description in the contract. | No |
| `variant` | `"primary" \| "secondary" \| "tertiary"` | `primary` | Visual weight. | No |
| `tone` | `"info" \| "success" \| "warning" \| "error" \| "neutral"` | `neutral` | Semantic color, orthogonal to `variant`. | No |
| `size` | `"sm" \| "md" \| "lg"` | `md` | Size. | No |
| `surface` | `"default" \| "onDark" \| "onBrand"` | `default` | Surface the button renders on; prefer this over absolute colors on tinted bands. | No |
| `disabled` | `boolean` | `false` | Disables interaction and dims the control. | No |
| `loading` | `boolean` | `false` | Shows a loading state and blocks interaction; sets `aria-busy`. | No |
| `rounded` | `boolean` | `false` | Fully rounded (pill) corners. | No |
| `icon` | `React.ReactNode \| string` | none | Leading icon: a React node, a component, or a Phosphor icon name (e.g. `"arrow-left"`). | No |
| `endIcon` | `React.ReactNode \| string` | none | Trailing icon: a React node, a component, or a Phosphor icon name. | No |
| `children` | `React.ReactNode` | none | Button label. | No |
| `accessibleName` | `string` | none | Accessible name; required for icon-only buttons. Maps to `aria-label`. | No |
| `accessibleNameRef` | `string` | none | Id of an element that labels this button. Maps to `aria-labelledby`. | No |
| `accessibleDescription` | `string` | none | Extra context for assistive tech. Maps to `aria-describedby`. | No |
| `tooltip` | `string` | none | Native tooltip text (`title`). Becomes the `aria-label` only on icon-only buttons; with visible children the label stays the accessible name. | No |
| `consequence` | `"reversible" \| "irreversible" \| "external" \| "financial" \| "privacy" \| "identity"` | none | Class of action this button performs. `irreversible` requires `tone="error"` and an explicit confirmation step; `external` (sends something to someone) requires a review step; `reversible` requires an undo. Rendered as `data-consequence` for tests and audits. | No |

## Variants

- Available variants: `primary`, `secondary`, `tertiary` (default `primary`).
- Available tones: `neutral`, `error`, `warning`, `success`, `info` (default `neutral`).
- Available surfaces: `default`, `onDark`, `onBrand` (default `default`).
- Available sizes: `sm`, `md`, `lg` (default `md`).
- Allowed `target` values: `_self`, `_blank`, `_parent`, `_top`.
- Allowed `consequence` values: `reversible`, `irreversible`, `external`, `financial`, `privacy`, `identity`.

## Invalid combinations

- MUST NOT: When `icon` is set and `children` is not set, then set one of `accessibleName`, `accessibleNameRef`, `tooltip`, `aria-label`. An icon-only button has no accessible name. Add accessibleName, accessibleNameRef, or tooltip, e.g. <Button icon="search" accessibleName="Search" />.
- MUST NOT: When `tone` is one of `error`, `warning`, `success`, `info` and `icon` is not set and `children` is not set, then set one of `accessibleName`, `accessibleNameRef`, `tooltip`, `aria-label`. A tone other than neutral renders its status icon by default, so a Button with a tone and no children is icon-only and needs an accessible name, e.g. <Button tone="error" accessibleName="Delete" />.
- MUST NOT: When `consequence` is `irreversible` and `tone` is one of `neutral`, `info`, `success`, `warning`, then do not set `tone`. An irreversible action must read as destructive before it is activated. Use tone="error", e.g. <Button consequence="irreversible" tone="error">Delete</Button>.
- MUST NOT: When `consequence` is `irreversible` and `tone` is not set, then also set `tone`. An irreversible action defaults to the neutral tone, which hides its consequence. Set tone="error".
- MUST NOT: `href` and `submits` set together. `href` and `submits` never coexist in a valid ButtonProps branch.
- MUST NOT: `clickAction` and `href` set together. `clickAction` and `href` never coexist in a valid ButtonProps branch.
- MUST: `target` set without `href`. Whenever `target` is accepted by ButtonProps, `href` must also be present.
- MUST: `rel` set without `href`. Whenever `rel` is accepted by ButtonProps, `href` must also be present.

## Keyboard

| Key | Behaviour |
| --- | --- |
| Enter | Activates the focused control. (standard) |
| Space | Activates or toggles the focused control. (standard) |

Rows marked (standard) name a key the contract lists without a description; the behaviour shown is the usual one for that key.

## 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 NOT | When `icon` is set and `children` is not set, then set one of `accessibleName`, `accessibleNameRef`, `tooltip`, `aria-label`. An icon-only button has no accessible name. Add accessibleName, accessibleNameRef, or tooltip, e.g. <Button icon="search" accessibleName="Search" />. | Checked by the MCP tool validate_component_usage (rule `button-icon-only-needs-name`). Also enforced in code: Button.tsx development warning (iconOnly && !hasAccessibleName). |
| MUST NOT | When `tone` is one of `error`, `warning`, `success`, `info` and `icon` is not set and `children` is not set, then set one of `accessibleName`, `accessibleNameRef`, `tooltip`, `aria-label`. A tone other than neutral renders its status icon by default, so a Button with a tone and no children is icon-only and needs an accessible name, e.g. <Button tone="error" accessibleName="Delete" />. | Checked by the MCP tool validate_component_usage (rule `button-status-icon-only-needs-name`). Also enforced in code: Button.tsx development warning (iconOnly && !hasAccessibleName, checked against the resolved icon). |
| MUST NOT | When `consequence` is `irreversible` and `tone` is one of `neutral`, `info`, `success`, `warning`, then do not set `tone`. An irreversible action must read as destructive before it is activated. Use tone="error", e.g. <Button consequence="irreversible" tone="error">Delete</Button>. | Checked by the MCP tool validate_component_usage (rule `button-irreversible-needs-error-tone`). Also enforced in code: Button.tsx development warning (consequence irreversible without tone error). |
| MUST NOT | When `consequence` is `irreversible` and `tone` is not set, then also set `tone`. An irreversible action defaults to the neutral tone, which hides its consequence. Set tone="error". | Checked by the MCP tool validate_component_usage (rule `button-irreversible-tone-missing`). Also enforced in code: Button.tsx development warning (consequence irreversible without tone error). |
| MUST NOT | `href` and `submits` set together. `href` and `submits` never coexist in a valid ButtonProps branch. | Checked by the MCP tool validate_component_usage. |
| MUST NOT | `clickAction` and `href` set together. `clickAction` and `href` never coexist in a valid ButtonProps branch. | Checked by the MCP tool validate_component_usage. |
| MUST | `target` set without `href`. Whenever `target` is accepted by ButtonProps, `href` must also be present. | Checked by the MCP tool validate_component_usage. |
| MUST | `rel` set without `href`. Whenever `rel` is accepted by ButtonProps, `href` must also be present. | Checked by the MCP tool validate_component_usage. |
| MUST | ARIA: Accessible name. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Aria-busy when loading. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Disabled state. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Aria-disabled (not disabled) in link mode; anchors have no disabled attribute. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: Focus-visible ring via --focus-ring-color; Highlight outline under forced colors. | Not yet verified: no evidence record. WCAG 4.1.2. |
| 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. | Manual review, last on 2026-11-04. |
| 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-11-04. |
| MUST | When clickAction returns a promise, the button is disabled and sets aria-busy on the same render, and repeat activations while pending do not call clickAction again. | Automated by `Button.temporal.test.tsx`. |
| MUST | When the pending promise resolves or rejects, the button returns to idle; a rejection never leaves it disabled. | Automated by `Button.temporal.test.tsx`. |
| SHOULD | Reserve variant=primary for the single most important action in the view; use secondary or tertiary for everything else. | Not stated in the contract. |
| SHOULD | Write labels that name the action, like Save changes or Send message, never OK or Click here. | Not stated in the contract. |
| SHOULD | Use clickAction for async work so the button shows its loading state and dedupes clicks automatically; if you need a progress readout (e.g. percent complete), render it yourself in children, the component only owns the busy/disabled state. | Not stated in the contract. |
| SHOULD | Pair tone=error with a confirmation step for irreversible actions. | Not stated in the contract. |
| SHOULD | Use surface=onDark or onBrand when the button sits on a dark or brand background instead of overriding colors. | Not stated in the contract. |
| SHOULD NOT | Do not place more than one primary button in the same view; it dilutes the hierarchy. | Not stated in the contract. |
| SHOULD NOT | Do not encode meaning with tone alone; the label must carry the meaning for color-blind users. | Not stated in the contract. |

## Install and import

- Install: `npm install @digitaltableteur/react` (version 0.9.1, restricted access on npm).
- Import: `import { Button } 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-button>`; see the custom elements manifest.

## Links

- Machine-readable contract (Design System Contract 1.0): https://www.digitaltableteur.com/contracts/v1/Button.contract.json
- Markdown version of this page: https://www.digitaltableteur.com/design-system/components/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.
