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-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 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+--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