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. Markdown version: /design-system/components/button.md
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(defaultprimary). - Available tones:
neutral,error,warning,success,info(defaultneutral). - Available surfaces:
default,onDark,onBrand(defaultdefault). - Available sizes:
sm,md,lg(defaultmd). - Allowed
targetvalues:_self,_blank,_parent,_top. - Allowed
consequencevalues:reversible,irreversible,external,financial,privacy,identity.
Invalid combinations
- MUST NOT: When
iconis set andchildrenis not set, then set one ofaccessibleName,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
toneis one oferror,warning,success,infoandiconis not set andchildrenis not set, then set one ofaccessibleName,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
consequenceisirreversibleandtoneis one ofneutral,info,success,warning, then do not settone. 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
consequenceisirreversibleandtoneis not set, then also settone. An irreversible action defaults to the neutral tone, which hides its consequence. Set tone="error". - MUST NOT:
hrefandsubmitsset together.hrefandsubmitsnever coexist in a valid ButtonProps branch. - MUST NOT:
clickActionandhrefset together.clickActionandhrefnever coexist in a valid ButtonProps branch. - MUST:
targetset withouthref. Whenevertargetis accepted by ButtonProps,hrefmust also be present. - MUST:
relset withouthref. Wheneverrelis accepted by ButtonProps,hrefmust 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/actionsexports it too. - Styles: import
@digitaltableteur/react/style.cssonce 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)
- 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.