Buttons trigger immediate actions; navigation belongs to Link, though a link may wear the button classes — as a link, Space does not activate and the disabled and busy states do not apply. Choose intent first, then size.
The size names remain Small, Medium, and Large; Medium is the modifier-free default. Buttons scale on both axes: text edges use space.2 / space.3 / space.4, height and owned icon size move with the same size, and the content gap stays at space.2.
| Example | Size | Modifier | Height | Usage |
|---|---|---|---|---|
| Small | --sm | size.interactive.sm | Dense surfaces; button-sm type with space.2 inline padding | |
| Medium · default | ds-button | size.interactive.md | Forms and most surfaces; button-md type with space.3 inline padding — the control inset | |
| Large | --lg | size.interactive.lg | High-emphasis flows; button-lg type with space.4 inline padding |
Type encodes intent, never decoration — choose by what the action means.
| Example | Type | Modifier | Roles | Usage |
|---|---|---|---|---|
| Primary | --primary | action.primary.* | The view's main action — one per view | |
| Secondary | --secondary | inherit · currentColor | Supporting actions on any paired surface; the same-hue outline keeps them quiet | |
| Ghost | --ghost | control.* | Lowest emphasis: toolbars, dense UI, repeated row actions | |
| Danger | --danger | action.danger.* | Destructive actions; confirm before irreversible ones |
Every type at every size, side by side — intent first, then size.
Icon-only buttons require an accessible name that states the action, not the icon. The icon tracks the control size — sm with icon.sm, md with icon.md, lg with icon.lg.
| Example | Size | Modifiers | Icon | Usage |
|---|---|---|---|---|
| Small | --icon--sm | icon.sm | Dense toolbars, table row actions | |
| Medium | --icon | icon.md | The default icon action | |
| Large | --icon--lg | icon.lg | High-emphasis, touch targets | |
| Small | --ghost--icon--sm | icon.sm | Canonical standalone close or dismiss action for Sheet, Banner, Toast, and other surfaces |
Shape is orthogonal to type: --rounded turns any icon-only button into a circle — square and circle across primary, secondary, and ghost.
Pair icons with labels when the action benefits from quick scanning — the label leads, the icon supports. Labeled buttons pair icon.sm at sm and md; large steps up to icon.md. Text edges scale through space.2 / space.3 / space.4; an occupied icon edge is one step tighter at space.1 / space.2 / space.3.
| Example | Icon | Modifiers | Padding | Usage |
|---|---|---|---|---|
| Leading, small | --sm--leading-icon | space.1space.2 | Compact button geometry scales on both edges | |
| Leading | --leading-icon | space.2space.3 | Icon reinforces the verb | |
| Leading, large | --lg--leading-icon | space.3space.4 | Type, icon, and inset step up together | |
| Trailing, small | --sm--trailing-icon | space.2space.1 | Compact button geometry scales on both edges | |
| Trailing | --trailing-icon | space.3space.2 | Direction or consequence follows the label | |
| Trailing, large | --lg--trailing-icon | space.4space.3 | Type, icon, and inset step up together | |
| Both, small | --sm--leading-icon--trailing-icon | space.1space.1 | Both occupied edges use the small optical inset | |
| Both | --leading-icon--trailing-icon | space.2space.2 | Rare — paired directions | |
| Both, large | --lg--leading-icon--trailing-icon | space.3space.3 | Both occupied edges use the large optical inset |
Set aria-busy="true" instead of swapping in a spinner — the button stays focusable and announces the busy state.
The spinner joins as a prefix and replaces any icons; the label stays visible and in the accessibility tree.
Every type answers every state in its own grammar: in the monochrome base, primary and danger drop their fills to text-grade outlines on hover and return to filled active states; secondary strengthens its outline, and ghost changes ink plus plate presence. Real DOM drivers — :hover, :active, :focus-visible, disabled, aria-busy — are forced here with demo classes and behave identically.
| Type | Rest | Hover | Active | Focus | Disabled | Loading |
|---|---|---|---|---|---|---|
--primary | ||||||
--secondary | ||||||
--ghost | ||||||
--danger |
Button choices encode intent, not decoration.
| Verdict | Rule |
|---|---|
| Do | Label verb + noun in sentence case (Deploy project, Invite member) — never bare Submit, OK, or Confirm. |
| Do | Keep one primary per view; supporting actions are secondary or ghost. |
| Do | Reserve danger for destructive actions and pair irreversible ones 1:1 with a confirmation and an outcome toast (Delete project → Project deleted). |
| Do | Set loading via aria-busy instead of swapping in a spinner — the button stays focusable and announces the busy state. |
| Do | Give icon-only buttons an accessible name stating action and target (Copy deployment URL), not the icon (Copy). |
| Do | Always set an explicit type attribute — a bare <button> inside a form silently submits. |
| Don’t | Disable as validation feedback — disable only when the action is impossible right now, and explain why nearby. |
| Don’t | Set aria-label on a button with visible text — it creates a screen-reader mismatch. |
| Don’t | Add visual weight when disabling — disabled flattens color but keeps the variant's weight ladder: fill, outline, bare text. |
| Don’t | Override colors per instance — rebranding happens at the ramp level, never on one button. |