Spec version v0.9.0

Button

Purpose

A Button is the primary mechanism for triggering actions. Use it when a user needs to submit a form, confirm a dialog, navigate to a creation flow, or execute a command. Do NOT use a Button when the destination is a URL and the user expects browser link behavior (e.g., right-click to open in a new tab) — use a Link or an anchor element instead. Do NOT use a Button to toggle a persistent setting that has an immediate effect on visible state; use a Toggle or Switch instead.

Anatomy

A Button is a single interactive element composed of:

  • Label (required for all variants except icon-only): a short verb phrase describing the action. Use sentence case. Keep to three words or fewer.
  • Leading icon (optional): a Material Symbols Rounded icon placed to the left of the label. Use to reinforce the action type (e.g., a plus icon for "Add item").
  • Trailing icon (optional): a Material Symbols Rounded icon placed to the right of the label. Use sparingly — only when the icon communicates something the label alone does not (e.g., a chevron-right indicating the action opens a new page or panel).
  • Icon-only (optional modifier): removes the label entirely. Always provide an accessible label via aria-label when using icon-only mode. Icon-only buttons should be used only in space-constrained surfaces such as toolbars and table action columns.

A Button does not contain arbitrary HTML beyond these parts. Do not nest other interactive elements inside a Button.

Variants

The system defines five visual variants combined with color intent. The valid combinations are:

variant: primary / color: confirm

The single most visually prominent button on a surface. Use for the primary call-to-action: saving a form, completing a wizard step, or confirming a modal. There MUST be no more than one primary-confirm button visible at a time within a given context (a card, a form, a modal footer). Background is --force-color-bg-primary (indigo-500), text is --force-color-text-on-primary (white).

variant: secondary / color: confirm

Use for the secondary action that accompanies a primary-confirm button, when that secondary action is still brand-aligned. Less common than secondary-default. Examples: "Save draft" alongside "Publish." Background is --force-color-bg-primary-subtle (indigo-50), text is --force-color-text-primary-brand (indigo-500).

variant: primary / color: default

A filled neutral button. Use for the primary action in a context where brand color would be visually inappropriate or where the surface already carries heavy brand color (e.g., inside a branded header). Background is --force-color-bg-emphasis (neutral-100), text is --force-color-text-primary.

variant: secondary / color: default

A bordered, transparent-background button. Use for secondary or tertiary actions that do not require fill emphasis. This is the most common non-primary button in the system: "Cancel," "Back," "Edit," "Export." Background is transparent with a --force-color-border-default border, text is --force-color-text-primary.

variant: tertiary / color: default

A borderless, transparent-background button. Use for low-emphasis actions in crowded surfaces, such as inline form actions or table row controls, where even a border would add unwanted visual weight. Do NOT use tertiary when the action has significant consequence — the low visual weight implies low consequence.

variant: subtle / color: default

A lightly filled neutral button with no border. Background is --force-color-bg-muted (neutral-50), text is --force-color-text-primary. Use in surfaces where a slight fill is needed to distinguish the button from its background without using brand color or a border. Common in toolbars and compact control bars.

variant: transparent / color: default

The ghost-style button. No border, no fill. Text and icon only. Reserve for actions embedded inline within other components (e.g., a "clear" action inside an input field, or a "View" action in a table cell alongside an icon). Do NOT use transparent buttons as standalone primary or secondary actions.

variant: primary / color: danger

A solid filled button in the destructive cranberry color. Use ONLY to confirm a destructive, irreversible action: deleting a resource, revoking access, wiping a dataset. This button MUST appear inside a confirmation dialog — never inline in a list or table row without a confirmation step. Background is --force-color-bg-destructive (cranberry-500), text is --force-color-text-on-primary (white). Do NOT use this variant for actions that are reversible or low-risk.

variant: primary / color: onBrand

Use when the button is placed on a branded (dark or indigo-colored) background such as the topbar or a hero banner. Background: --force-color-bg-on-brand (10% white over the brand surface). Hover: --force-color-bg-on-brand-hover (20% white). Text: --force-color-text-on-primary (white). Do NOT use this variant on standard white or grey surfaces — the semi-transparent fill will not read.

States

Default: The button is at rest, ready for interaction. Visual appearance follows the variant definitions above.

Hover: Background darkens or a subtle fill appears (depending on variant). For primary-confirm: background changes from --force-color-bg-primary to --force-color-bg-primary-hover (indigo-600). For secondary-default: background transitions to --force-color-bg-interactive-hover (neutral-50). For danger: background transitions to --force-color-bg-destructive-hover (cranberry-600). Transition duration is --force-duration-fast (150ms) with --force-easing-standard.

Active/Pressed: Background darkens one further step from hover. For primary-confirm: --force-color-bg-primary-active (indigo-700). For danger: --force-color-bg-destructive-active (cranberry-700). Apply transform: scale(0.98) to provide tactile feedback.

Focus (keyboard): A 3px focus ring appears around the button using --force-shadow-focus-ring (indigo-100 spread). The border transitions to --force-color-border-focus (indigo-500). Focus styles MUST be visible and may not be suppressed. Focus is applied via :focus-visible (not :focus) to avoid showing focus rings on mouse click.

Disabled: The button becomes non-interactive. Apply opacity: --force-opacity-disabled (0.5) to the entire element, not just the text. Background is --force-color-bg-muted, text is --force-color-text-disabled. The cursor changes to not-allowed. The disabled attribute MUST be set on the element so assistive technologies announce it as unavailable.

Loading: When an action is in progress, replace the label with a Spinner component and optionally a "Loading…" or action-specific label (e.g., "Saving…"). The button MUST remain disabled during the loading state (set disabled or aria-disabled). Maintain the button's current width to prevent layout shift; do not collapse the button to spinner size.

Behavior

  • Touch target: Minimum 32px height (sm), prefer 40px (md) or 48px (lg). Never place buttons closer than 8px apart edge-to-edge on touch surfaces.
  • Sizes: sm (32px height, 12px horizontal padding), md (40px height, 16px horizontal padding, default), lg (48px height, 20px horizontal padding). Icon size scales with button: sm uses 16px icons, md uses 20px icons, lg uses 24px icons. Gap between icon and label is 4px (sm) or 6px (md/lg).
  • Shape: Default is rounded (radius --force-radius-button, 6px). circular (pill, border-radius: 9999px) is available for icon-only buttons or special marketing contexts. square (no radius) should not be used in product UI.
  • Width: Buttons are inline (width: fit-content) by default and grow to accommodate their label. In form footer rows, use a Button Group or flex row to control alignment. Do NOT stretch a button to full width unless you are inside a narrow mobile container or an explicit full-width call-to-action section.
  • Keyboard: Buttons are activated by Space or Enter. Do not intercept these keys for custom behavior inside a button.
  • Form submission: A button with type="submit" inside a <form> will submit that form. When a button should NOT submit a form, explicitly set type="button".

Accessibility

  • The rendered element MUST be a <button> (or a link with role="button" only when rendering as an anchor, which is only appropriate for navigation actions). Do not use <div> or <span> with click handlers.
  • Provide an aria-label for icon-only buttons that clearly describes the action (e.g., aria-label="Delete dataset"). The label MUST be unique within the page context — do not use generic labels like "Delete" when multiple delete buttons exist on the same page; qualify them (e.g., "Delete dataset 'prod-clone'").
  • When a button triggers an action that is asynchronous, set aria-busy="true" and optionally aria-label="Saving…" during the loading state. Remove these attributes when the action completes.
  • Use aria-disabled="true" rather than the disabled attribute when you need the button to remain focusable while inactive (e.g., showing a tooltip explaining why it is disabled). Use the native disabled attribute when the button should be removed from the tab order entirely.
  • Danger confirmation dialogs that a button opens must be labeled with aria-haspopup="dialog" on the trigger button.
  • Button groups with related actions should be wrapped in a <div role="group" aria-label="...">.

Composition

Buttons appear most commonly in:

  • Form footers: A right-aligned row, typically "Cancel" (secondary) on the left and "Save" or "Submit" (primary-confirm) on the right.
  • Modal footers: Same pattern as form footers. For destructive modals: "Cancel" (secondary) and "Delete" (primary-danger).
  • Page headers (PageHeader): One primary action ("Create …") and optionally one secondary action ("Import").
  • Toolbar / ControlsBar: A row of secondary or tertiary buttons for filtering, exporting, and bulk operations.
  • Table action columns: Transparent ghost buttons ("View", "Edit") in a pinned right column.
  • Empty states: One primary action as the main call-to-action.

Buttons should NOT be used inside badges, alert banners (use a Link instead), or navigation items. Do not combine a primary-confirm button with a primary-danger button in the same button group — one context cannot have two primary destructive actions.

Guidance

Choose variant by asking: "How much visual weight should this action carry?"

  • One dominant action exists and it is constructive: use primary / confirm.
  • One dominant action exists and it is destructive: use primary / danger inside a confirmation dialog only.
  • Secondary support action (cancel, go back, export): use secondary / default.
  • Low-emphasis inline action (clear, undo, view details): use tertiary / default or transparent / default.
  • Action is on a brand-colored surface: use primary / onBrand.

Common mistakes to avoid:

  • Do NOT place two primary / confirm buttons next to each other. If you find yourself doing this, one of those actions should be secondary / default.
  • Do NOT use a primary / danger button without a confirmation step. Destructive actions must always require a second deliberate user confirmation.
  • Do NOT make every button sm in a form to save space. Default to md for form buttons; sm is for toolbars and table controls where density is required.
  • Do NOT disable a submit button simply because a form is empty. Disable it only when the form has been touched and is in an invalid state, or when a submission is already in progress.

Token Usage

primary / confirm (brand CTA)

  • Background: --force-color-bg-primary → hover: --force-color-bg-primary-hover → active: --force-color-bg-primary-active
  • Text: --force-color-text-on-primary
  • Border: none (transparent)
  • Focus ring: --force-shadow-focus-ring + --force-color-border-focus

secondary / default (bordered)

  • Background: transparent → hover: --force-color-bg-interactive-hover
  • Text: --force-color-text-primary
  • Border: --force-color-border-default
  • Focus ring: --force-shadow-focus-ring + --force-color-border-focus

tertiary / default (borderless)

  • Background: transparent → hover: --force-color-bg-interactive-hover
  • Text: --force-color-text-primary
  • Border: none
  • Focus ring: --force-shadow-focus-ring + --force-color-border-focus

primary / danger (destructive)

  • Background: --force-color-bg-destructive → hover: --force-color-bg-destructive-hover → active: --force-color-bg-destructive-active
  • Text: --force-color-text-on-primary
  • Border: none
  • Focus ring: --force-shadow-focus-ring-error + --force-color-border-error

Disabled state (all variants)

  • Background: --force-color-bg-muted
  • Text: --force-color-text-disabled
  • Opacity: --force-opacity-disabled (0.5 applied to whole element)

Typography

  • Font size: --force-font-size-sm (14px, md/lg), --force-font-size-xs (12px, sm)
  • Font weight: --force-font-weight-medium (500)
  • Border radius: --force-radius-button (6px)
  • Transition: --force-duration-fast (150ms) / --force-easing-standard