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-labelwhen 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-input 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 two-stop focus ring appears around the button via a box-shadow token. For the primary / confirm and primary / danger variants, whose backgrounds are brand or destructive fills, use --force-shadow-focus-ring-on-brand (inner stop matches the button fill, outer stop is white) so the ring is visible against the saturated background. For all other variants (secondary, tertiary, subtle, transparent, onBrand) whose resting background is white, muted, or near-white, use --force-shadow-focus-ring (inner white stop, outer indigo-500 outer stop). 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. Background is --force-color-bg-muted, text is --force-color-text-disabled, border (when the variant has one) is --force-color-border-muted. The cursor changes to not-allowed. The disabled attribute MUST be set on the element so assistive technologies announce it as unavailable. Do NOT apply an additional opacity multiplier on top of these tokens — text.disabled and bg.muted are already calibrated to communicate the disabled state, and an extra 0.5 opacity (used in earlier versions of this pattern) compounds with those tokens and renders disabled controls effectively invisible in dark mode.
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 settype="button".
Accessibility
- The rendered element MUST be a
<button>(or a link withrole="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-labelfor 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 optionallyaria-label="Saving…"during the loading state. Remove these attributes when the action completes. - Use
aria-disabled="true"rather than thedisabledattribute when you need the button to remain focusable while inactive (e.g., showing a tooltip explaining why it is disabled). Use the nativedisabledattribute 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 / dangerinside a confirmation dialog only. - Secondary support action (cancel, go back, export): use
secondary / default. - Low-emphasis inline action (clear, undo, view details): use
tertiary / defaultortransparent / default. - Action is on a brand-colored surface: use
primary / onBrand.
Common mistakes to avoid:
- Do NOT place two
primary / confirmbuttons next to each other. If you find yourself doing this, one of those actions should besecondary / default. - Do NOT use a
primary / dangerbutton without a confirmation step. Destructive actions must always require a second deliberate user confirmation. - Do NOT make every button
smin a form to save space. Default tomdfor form buttons;smis 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-on-brand+--force-color-border-focus— the inner stop matches the brand fill so the ring reads against an indigo background
secondary / default (bordered)
- Background: transparent → hover:
--force-color-bg-interactive-hover - Text:
--force-color-text-primary - Border:
--force-color-border-input - Focus ring:
--force-shadow-focus-ring+--force-color-border-focus— white inner stop, indigo outer; surface background provides the gap
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— white inner stop, indigo outer; surface background provides the gap
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— white inner stop provides the gap against the cranberry fill; cranberry outer band reinforces destructive intent
Disabled state (all variants)
- Background:
--force-color-bg-muted - Text:
--force-color-text-disabled - Border (variants with a border):
--force-color-border-muted - Cursor:
not-allowed - Do NOT apply an additional
opacitymultiplier — the muted bg + disabled text tokens already communicate disabled. Layering opacity on top compounds the muting and makes the control unreadable, especially in dark mode. (--force-opacity-disabledremains in the token set for non-button surfaces that need it; do not use it here.)
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