Button
Usage
Use a button to start an action on the current screen: save a work order, assign a mechanic, export a report. Use a link instead when the result is navigation to another page; a link styled as a button (<a class="btn">) is allowed only for that case.
- Primary — the one main action of a region (a form, a dialog, a panel). Teal is reserved for this; never two primaries side by side.
- Secondary — alternatives next to the primary ("Save draft" next to "Close work order").
- Ghost (tertiary) — low-emphasis actions in dense areas: table rows, toolbars, "Cancel".
- Destructive — deletes or retires something. Always confirm in an alert dialog or offer undo.
Labels: verb + object, sentence case, three words or fewer ("Assign mechanic", "Export service log"). Avoid "OK", "Submit", "Click here". Keep paired labels parallel ("Save draft" / "Close work order"). Allow +40% length for translations — buttons wrap their row, never truncate the label.
Anatomy
- Container —
<button type="button|submit" class="btn">, 8px radius, height from--button-height-* - Label —
.btn__label(required when an icon or spinner is present) - Leading or trailing icon (optional) —
svg.btn__icon,aria-hidden="true" - Spinner (loading only) —
.btn__spinner, overlays the label so the width doesn't change
Examples
States marked hover, focus-visible and active are frozen with .is-hover, .is-focus-visible, .is-active. Those classes share their rule with the real pseudo-class, so what you see is exactly what users get. Never use them in product code.
API
| Hook | Values | Purpose |
|---|---|---|
.btn | block class | Base button; without a variant it renders as secondary |
.btn--primary | --secondary | --ghost | --destructive | modifier class | Emphasis |
.btn--sm | --lg | modifier class | Size (md is default): 32 / 40 / 48px, same heights as .input--sm|lg |
.btn--full-width | modifier class | Fills its container (mobile forms, tablet task screens) |
.btn__label, .btn__icon, .btn__spinner | part classes | Label, decorative icon, loading spinner |
disabled | attribute | Unavailable and removed from the tab order |
aria-disabled="true" | attribute | Unavailable but focusable; pair with aria-describedby explaining why. Your click handler must ignore it. |
aria-busy="true" | attribute | Loading: spinner replaces the label visually; also set aria-disabled="true" and ignore repeat activations |
.is-hover | .is-focus-visible | .is-active | docs-only class | Freezes a state for documentation and visual tests |
Do and don't
Accessibility
| Key | Behavior |
|---|---|
| Tab | Moves focus to the button (skipped when disabled; kept when aria-disabled) |
| Enter / Space | Activates the button |
- Role: native
<button>(WAI-ARIA APG Button pattern). Always settype; without it a button inside a form submits that form by accident. - Name: the visible label. Icons inside a labelled button are
aria-hidden="true" focusable="false". Icon-only buttons belong to the icon-button component and needaria-label. - Loading: the label stays in the accessibility tree (hidden with
opacity, notvisibility), so the name doesn't change. Announce progress and the result in arole="status"region. - Focus: 2px teal ring with 2px offset from
--focus-ring-*(≥ 3:1 in every theme). It is never removed. - Target size: smallest button is 32px high and at least 32px wide (WCAG 2.5.8 needs 24px). Use
.btn--lg(48px) on tablet screens mechanics use with gloves. - Contrast: label/background pairs
color.action.*.fgoncolor.action.*.bgare checked at 4.5:1 byds check. Disabled buttons are exempt but still readable. - Forced colors: every variant keeps a 1px border so buttons stay visible when backgrounds are removed.
Tokens
| Custom property | Purpose |
|---|---|
--button-radius | Corner radius (→ --radius-md, 8px) |
--button-height-sm | -md | -lg | Heights (→ --size-control-*) |
--button-font-weight | Label weight (semibold) |
--color-action-{primary|secondary|ghost|danger}-{bg|bg-hover|bg-active|fg} | Variant colors per state |
--color-action-secondary-border | Secondary outline |
--color-bg-muted, --color-text-disabled | Disabled fill and label |
--focus-ring-width | -offset | -color | Focus ring |
--density-{comfortable|compact}-control-padding-x | Inline padding per density |
--icon-size-sm | -md, --icon-stroke | Icon and spinner size |
--motion-duration-fast, --motion-easing-standard | Color transitions |