Design System

Buttons

One button component carries every action in the system. Weight is a variant, scale is a size, and every measurement below comes from a token — there are no per-screen button styles.

What is this?

The V2 Button system: eight variants (primary, secondary, tertiary, subtle, outline, ghost, destructive, link), three sizes, a full-width option, leading/trailing/icon-only configurations, and IconButton plus ButtonGroup for compression and grouping.

When to use it

  • A person performs an action: create, save, export, delete, continue
  • A row, toolbar or card needs a compact action affordance
  • Two or more related actions belong to one visual set

How to use it

  • Exactly one primary action per view; step everything else down
  • Pass icons through leadingIcon / trailingIcon, never as bare children
  • Use IconButton with a label for anything without visible text

What to avoid

  • Legacy per-screen button colours — variants replace them
  • Two primary buttons competing in one view
  • A button for navigation (use link variant or an anchor)

Standardized V2 values

Every value resolves to an existing V1 token. Nothing here is a raw literal.

Button specification by size
PropertySmallMedium (default)Large
Height32px (h-8)40px (h-10)44px (h-11)
Horizontal paddingspace-3 (12px)space-4 (16px)space-5 (20px)
Gapspace-1.5 (6px)space-1.5 (6px)space-2 (8px)
Border radiusradius-md (8px)radius-md (8px)radius-md (8px)
Fontlabel-smlabel-mdlabel-lg
Font weightweight-semibold (600)weight-semibold (600)weight-semibold (600)
Icon sizeicon-xs (12px)icon-sm (16px)icon-md (20px)
Border widthborder-width-thin (1px)border-width-thinborder-width-thin
Focus ringds-focus-ringds-focus-ringds-focus-ring
Transitionds-transition (fast · ease-standard)ds-transitionds-transition
Touch targetds-target (≥24px / ≥44px coarse)intrinsicintrinsic

Button

The single action control of the system. Variant sets weight, size sets scale, and configuration props cover icons and width.

import { Button } from "@/design-system";

When to use

  • Submitting, creating, exporting, confirming or deleting
  • Any action that changes state rather than navigating away
  • Full width inside mobile sheets, drawers and narrow forms

When not to use

  • Pure navigation between pages — use the link variant or an anchor
  • Icon-only controls — use IconButton so a label is required
  • Mutually exclusive selection — use SegmentedControl or ToggleGroup

Variants

All eight variants at the default medium size.

Soft status actions

Use the subtle variant for supportive actions that need semantic colour without the weight of a solid button.

Semantic tones

`tone` colours a solid, outline or ghost button for genuinely success / warning / danger / info actions. Default tone is `brand`.

Icon-only parity

IconButton mirrors the Button variant + tone map, so an icon-only action never needs a bespoke style. Shown resting and disabled.

Sizes

Small, medium and large share one variant map.

States

Hover, pressed and focus are shown with the same tokens the pseudo-classes apply.

Default

Hover

Pressed

Focus

Disabled

Loading

Loading, interactive

Click to see the real loading state.

Icon usage

Leading, trailing, text + icon and icon-only.

Full width

fullWidth stretches the button for mobile sheets and narrow forms.

Accessibility

  • Every variant meets WCAG 2.2 AA contrast in both light and dark themes
  • focus-visible renders a 2px offset ring in every variant, including ghost and link
  • Small buttons and IconButtons carry ds-target: ≥24px pointer target, ≥44px under coarse pointers
  • Disabled uses explicit surface/text/border disabled tokens, never opacity alone
  • loading sets aria-busy, announces loadingLabel and blocks interaction while keeping width stable

Groups

ButtonGroup joins related actions; SegmentedControl holds a selection.

Attached group — related actions

One control, several actions.

Attached group — pill corners

radius='full' rounds the outer edges of a split action.

Spaced group — independent actions

Segmented control — mutually exclusive views

Usage examples

Buttons in the shape they actually appear in the application.

Table toolbar

One primary action (Add resident); the toolbar action steps down to tertiary.

Destructive confirmation

Deleting this recurring schedule removes every future occurrence. This cannot be undone.

Form footer

Stacks vertically on mobile with fullWidth buttons in reverse importance order.

Mobile sheet

Do / Don't

Do

  • Use exactly one primary button per view and step the rest down.
  • Pass icons via leadingIcon / trailingIcon so gap and icon size stay tokenized.
  • Use IconButton with a label for icon-only controls, and a Tooltip when the meaning is not obvious.
  • Reach for fullWidth on mobile sheets and narrow forms instead of custom widths.
  • Confirm every destructive button through a confirm dialog.

Don't

  • Don't recreate the legacy palette of green/orange/red buttons — variants carry meaning now.
  • Don't add one-off classes for height, padding, radius or font size.
  • Don't put an icon in children as a bare element; it loses the tokenized icon size.
  • Don't use a button for navigation, or a link for a state-changing action.
  • Don't disable a button without telling people what is missing.

Why it is designed this way

Legacy Centrim screens carried at least eight visually distinct button styles for the same semantic action. V2 collapses them into one CVA map: colour communicates weight and intent, never a feature area.

Loading keeps the label in the flow (invisible but measured) so the button never changes width mid-request, and disabled uses dedicated disabled tokens so the label stays readable while emphasis clearly drops.