Skip to main content

Design system components

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

Button props
PropTypeDefaultDescriptionRequired
submitsbooleannoneWhen true, sets type="submit".No
clickAction(event: React.MouseEvent<HTMLButtonElement>) => void | Promise<void>noneAsync 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
hrefstringnoneDestination URL. When provided, the button renders as an anchor.No
target"_self" | "_blank" | "_parent" | "_top"noneNo description in the contract.No
relstring | undefinednoneNo description in the contract.No
variant"primary" | "secondary" | "tertiary"primaryVisual weight.No
tone"info" | "success" | "warning" | "error" | "neutral"neutralSemantic color, orthogonal to variant.No
size"sm" | "md" | "lg"mdSize.No
surface"default" | "onDark" | "onBrand"defaultSurface the button renders on; prefer this over absolute colors on tinted bands.No
disabledbooleanfalseDisables interaction and dims the control.No
loadingbooleanfalseShows a loading state and blocks interaction; sets aria-busy.No
roundedbooleanfalseFully rounded (pill) corners.No
iconReact.ReactNode | stringnoneLeading icon: a React node, a component, or a Phosphor icon name (e.g. "arrow-left").No
endIconReact.ReactNode | stringnoneTrailing icon: a React node, a component, or a Phosphor icon name.No
childrenReact.ReactNodenoneButton label.No
accessibleNamestringnoneAccessible name; required for icon-only buttons. Maps to aria-label.No
accessibleNameRefstringnoneId of an element that labels this button. Maps to aria-labelledby.No
accessibleDescriptionstringnoneExtra context for assistive tech. Maps to aria-describedby.No
tooltipstringnoneNative 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"noneClass 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

Button keyboard behaviour
KeyBehaviour
EnterActivates the focused control. (standard)
SpaceActivates 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.

Button rules
LevelRuleHow it is checked
MUST NOTWhen 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 NOTWhen 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 NOTWhen 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 NOTWhen 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 NOThref and submits set together. href and submits never coexist in a valid ButtonProps branch.Checked by the MCP tool validate_component_usage.
MUST NOTclickAction and href set together. clickAction and href never coexist in a valid ButtonProps branch.Checked by the MCP tool validate_component_usage.
MUSTtarget set without href. Whenever target is accepted by ButtonProps, href must also be present.Checked by the MCP tool validate_component_usage.
MUSTrel set without href. Whenever rel is accepted by ButtonProps, href must also be present.Checked by the MCP tool validate_component_usage.
MUSTARIA: Accessible name.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: Aria-busy when loading.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: Disabled state.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: Aria-disabled (not disabled) in link mode; anchors have no disabled attribute.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTARIA: Focus-visible ring via --focus-ring-color; Highlight outline under forced colors.Not yet verified: no evidence record. WCAG 4.1.2.
MUSTThe 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.
MUSTThe component remains legible and operable under a real browser forced-colors: active pass.Manual review, last on 2026-11-04.
MUSTEvery documented keyboard interaction works as specified.Not yet verified: no evidence record. WCAG 2.1.1, 2.1.2.
MUSTA human completed an end-to-end accessibility review of this component.Manual review, last on 2026-11-04.
MUSTWhen 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.
MUSTWhen the pending promise resolves or rejects, the button returns to idle; a rejection never leaves it disabled.Automated by Button.temporal.test.tsx.
SHOULDReserve variant=primary for the single most important action in the view; use secondary or tertiary for everything else.Not stated in the contract.
SHOULDWrite labels that name the action, like Save changes or Send message, never OK or Click here.Not stated in the contract.
SHOULDUse 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.
SHOULDPair tone=error with a confirmation step for irreversible actions.Not stated in the contract.
SHOULDUse surface=onDark or onBrand when the button sits on a dark or brand background instead of overriding colors.Not stated in the contract.
SHOULD NOTDo not place more than one primary button in the same view; it dilutes the hierarchy.Not stated in the contract.
SHOULD NOTDo 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.