Button

Action or navigation invoker

Intent

Button is the action atom: submit a form, open a dialog, remove a row, trigger a side effect. Render as <button> for actions (anything that mutates state or invokes a widget), as <a class="button"> for navigation that visually reads as a button (a primary CTA linking to another page, a download link). Don't invert the pair: an anchor that performs an action, or a button that navigates, confuses assistive tech and the browser's default behavior.

Paired with the command and commandfor attributes, buttons invoke native overlays without JavaScript. See dialog and drawer for live invoker examples; the button itself is just a button in that pattern.

Basic usage

A bare .button renders as a filled primary action -- the most visually prominent variant, meant for a page's single primary action. Everything else (outlined, ghost, icon, block) comes from modifiers.

<button class="button" type="button">Save changes</button>

Variants

Color and emphasis

Emphasis levels cover the common action hierarchy. Use primary (the default, no modifier) for a page's main action; .is-outlined for a less-prominent companion; .is-accent for a brand-accented alternate; .is-danger for destructive actions; .is-ghost for toolbar-level actions that shouldn't pull visual weight; .is-bare when the button wraps custom content (an avatar, an icon widget) and needs no chrome.

<div class="cluster is-tight">
    <button class="button" type="button">Primary</button>
    <button class="button is-neutral" type="button">Neutral</button>
    <button class="button is-accent" type="button">Accent</button>
    <button class="button is-danger" type="button">Danger</button>
    <button class="button is-ghost" type="button">Ghost</button>
    <button class="button is-bare" type="button">Bare</button>
</div>
<div class="cluster is-tight">
    <button class="button is-outlined" type="button">Outlined</button>
    <button class="button is-neutral is-outlined" type="button">Neutral (outlined)</button>
    <button class="button is-accent is-outlined" type="button">Accent (outlined)</button>
    <button class="button is-danger is-outlined" type="button">Danger (outlined)</button>
</div>

Hover darkens the fill, :active inverts the shadow inward for a pressed look, and :focus-visible draws the ring.

Size

Sizes key to the ui-pad-* density grammar. .is-sm tightens padding and drops the font one step; .is-lg relaxes padding and steps the font up.

<div class="cluster is-tight">
    <button class="button is-sm" type="button">Small</button>
    <button class="button" type="button">Default</button>
    <button class="button is-lg" type="button">Large</button>
</div>

Shape

Shape modes beyond the default text-plus-optional-icon row: .as-icon for icon-only buttons (equal padding on both axes via the ui-pad-square-* grammar), and .as-block for full-width buttons (form submit actions, modal-footer primaries). .as-icon requires aria-label -- the icon alone has no accessible name.

<div class="stack is-snug">
    <div class="cluster is-tight">
        <button class="button" type="button">Default shape</button>
        <button class="button is-outlined as-icon" type="button" aria-label="Settings">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                <circle cx="12" cy="12" r="3"></circle>
                <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
            </svg>
        </button>
    </div>
    <button class="button as-block" type="button">Full-width action</button>
</div>

States

Interactive state binds to native or ARIA attributes, not classes: :disabled (or [aria-disabled="true"] on non-button invokers) for unavailable actions, and [aria-busy="true"] for pending actions. The busy state renders a CSS spinner via ::before without changing the button's width, so the layout doesn't jump as the action progresses.

<div class="cluster is-tight">
    <button class="button" type="button" disabled="">Disabled</button>
    <button class="button is-outlined" type="button" disabled="">Disabled outlined</button>
    <button class="button" type="button" aria-busy="true">Saving</button>
    <button class="button is-outlined" type="button" aria-busy="true">Saving</button>
    <button class="button is-danger" type="button" aria-busy="true">Deleting</button>
    <button class="button is-danger is-outlined" type="button" aria-busy="true">Deleting</button>
</div>

Pick by meaning: disabled means "this action cannot be taken right now"; aria-busy means "this action has been taken and is in progress". Using disabled during a save is technically available but semantically wrong -- the user is waiting on a response, not being refused. prefers-reduced-motion slows the spinner rotation so it stays visible but isn't disorienting.

Composed patterns

Buttons with icons

Drop an SVG as a sibling of the label text. The button's own gap spaces them; the SVG sizes itself via --button-icon-size (falls back to --icon-size). No .button-icon slot class -- the SVG sits where it's authored.

<div class="cluster is-tight">
    <button class="button is-outlined" type="button">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
            <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
            <polyline points="7 10 12 15 17 10"></polyline>
            <line x1="12" y1="15" x2="12" y2="3"></line>
        </svg>
        Download
    </button>
    <button class="button" type="button">
        Continue
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
            <polyline points="9 18 15 12 9 6"></polyline>
        </svg>
    </button>
</div>

Button group

For tightly-coupled action sets -- segmented controls, view switchers, mode pickers -- .button-group joins its .button children edge-to-edge into one visual unit. Adjacent borders collapse into a shared edge, outer corners keep their radius, and the selected member binds to [aria-pressed="true"] so the chosen visual follows the announced state directly.

<div class="stack is-snug">
    <div class="cluster is-tight">
        <div class="button-group">
            <button class="button" type="button">Bold</button>
            <button class="button" type="button" aria-pressed="true">Italic</button>
            <button class="button" type="button">Underline</button>
        </div>
        <div class="button-group">
            <button class="button is-neutral" type="button">Bold</button>
            <button class="button is-neutral" type="button" aria-pressed="true">Italic</button>
            <button class="button is-neutral" type="button">Underline</button>
        </div>
    </div>
    <div class="cluster is-tight">
        <div class="button-group">
            <button class="button is-accent" type="button">Bold</button>
            <button class="button is-accent" type="button" aria-pressed="true">Italic</button>
            <button class="button is-accent" type="button">Underline</button>
        </div>

        <div class="button-group">
            <button class="button is-danger" type="button">Bold</button>
            <button class="button is-danger" type="button" aria-pressed="true">Italic</button>
            <button class="button is-danger" type="button">Underline</button>
        </div>
    </div>

    <div class="cluster is-tight">
        <div class="button-group">
            <button class="button is-outlined" type="button">Bold</button>
            <button class="button is-outlined" type="button" aria-pressed="true">Italic</button>
            <button class="button is-outlined" type="button">Underline</button>
        </div>
        <div class="button-group">
            <button class="button is-neutral is-outlined" type="button">Bold</button>
            <button class="button is-neutral is-outlined" type="button" aria-pressed="true">Italic</button>
            <button class="button is-neutral is-outlined" type="button">Underline</button>
        </div>
    </div>
    <div class="cluster is-tight">
        <div class="button-group">
            <button class="button is-accent is-outlined" type="button">Bold</button>
            <button class="button is-accent is-outlined" type="button" aria-pressed="true">Italic</button>
            <button class="button is-accent is-outlined" type="button">Underline</button>
        </div>

        <div class="button-group">
            <button class="button is-danger is-outlined" type="button">Bold</button>
            <button class="button is-danger is-outlined" type="button" aria-pressed="true">Italic</button>
            <button class="button is-danger is-outlined" type="button">Underline</button>
        </div>
    </div>

    <div class="button-group">
        <button class="button" type="button">Foo</button>
        <button class="button" type="button">Bar</button>
        <button class="button" type="button">Baz</button>
        <button class="button" type="button">Qux</button>
    </div>

    <div class="button-group">
        <button class="button is-sm" type="button">Foo</button>
        <button class="button is-sm" type="button">Bar</button>
        <button class="button is-sm" type="button">Baz</button>
        <button class="button is-sm" type="button">Qux</button>
    </div>

    <div class="button-group">
        <button class="button" type="button">Foo</button>
        <button class="button" type="button" aria-pressed="true">Bar</button>
        <button class="button" type="button">Baz</button>
        <button class="button" type="button">Qux</button>
    </div>

    <div class="button-group">
        <button class="button is-outlined" type="button">Foo</button>
        <button class="button is-outlined" type="button" aria-pressed="true">Bar</button>
        <button class="button is-outlined" type="button">Baz</button>
        <button class="button is-outlined" type="button">Qux</button>
    </div>
</div>

The pressed member sits on a tinted-light fill of its own family with text that has guaranteed contrast against it. On solid (non-outlined) members, a family-color border separates the lightened pressed fill from the solid neighbors so the selection reads as a deliberate edge rather than just a lightness drop. Outlined members already carry a tinted border and don't need the extra emphasis.

Button cluster

For independent actions sitting together -- cancel-plus-confirm pairs, multi-step wizards, modal footer actions -- use .cluster.is-tight. Cluster spaces buttons with a fluid gap and wraps them gracefully on narrow viewports; reach for it when the buttons aren't a single segmented unit.

<div class="cluster is-tight">
    <button class="button is-outlined" type="button">Cancel</button>
    <button class="button" type="button">Save</button>
</div>

Class reference

  • .button -- the atom. Render as <button> for actions, <a class="button"> for navigation.
  • .is-outlined, .is-accent, .is-danger, .is-ghost, .is-bare -- emphasis variants. Default (no modifier) is primary.
  • .is-sm, .is-lg -- size variants.
  • .as-icon -- icon-only shape (square padding). Requires aria-label.
  • .as-block -- full-width shape.
  • .button-group -- joins .button children edge-to-edge into a segmented set. Selected member binds to [aria-pressed="true"] and picks up --color-state-bg.

Interactive state is attribute-driven: disabled (or [aria-disabled="true"]) and [aria-busy="true"]. No .is-disabled or .is-loading classes.

Customization

Button exposes scoped tokens for color (bg, text, hover text, border, shadow), sizing (padding, radius, font size), and icon sizing. Emphasis variants remap the color tokens; size variants remap padding and font-size; .as-icon swaps to the ui-pad-square-* grammar; .is-bare zeroes padding and radius to turn the button into a plain clickable wrapper.

Full token list and defaults: src/css/button.css.

Accessibility

  • Pick <button> vs <a class="button"> by behavior. Action (submits, deletes, opens a widget) always uses <button> -- anchors don't carry action semantics and Enter/Space handling differs. Navigation (goes to a URL) uses <a class="button"> with a real href. Never role="button" on an anchor unless you're overriding its navigational meaning, which is rare.
  • .as-icon requires a name. An icon-only button has no visible text, so aria-label is mandatory. A .visually-hidden span with the label text works too and survives a CSS-disabled render.
  • disabled vs aria-busy. disabled tells the user "this action is unavailable"; aria-busy="true" tells the user "this action is in progress". Screen readers announce the difference. Use disabled for validation failures and missing inputs; use aria-busy for "clicked save, now waiting on the network".
  • [aria-disabled="true"] for non-button invokers. Anchors can't carry the native disabled attribute but can carry aria-disabled; button styles both the same way, and pointer-events: none keeps the element inert in either case.

CSS Reference: Button

Clickable action trigger

Source: src/css/button.css

Tokens

TokenDefaultDescription
--button-bgvar(--color-primary)Background color for the button
--button-colorvar(--color-on-primary)Text and icon color for the button
--button-color-hovervar(--button-color)Text color on hover, defaults to --button-color
--button-bordertransparentBorder color for the button
--button-padding-blockvar(--ui-pad-y-tight)Vertical padding inside the button
--button-padding-inlinevar(--ui-pad-x-snug)Horizontal padding inside the button
--button-radiusvar(--radius-md)Border radius for the button
--button-font-sizevar(--ui-text-sm)Font size for button text
--button-font-weightvar(--font-weight-bold)Font weight for button text
--button-icon-sizevar(--icon-sizeSize of SVG icons inside the button

Slots

SlotDescription
svgIcons act as their own slot inside a button

Classes

ClassDescription
.buttoncomponent root
.[aria-busy="true"]Setting aria-busy=true will show the spinner indicator, and alter the cursor type.
.is-accentAccent color variant
.is-dangerDanger color variant
.is-neutralNeutral fill for chrome and quiet actions
.is-outlinedTransparent fill with tinted text and border
.is-ghostTransparent background with no border
.is-bareNo visual chrome, just a clickable wrapper
.is-smSmaller size with tighter padding
.is-lgLarger size with more padding
.as-iconSquare padding for icon-only buttons
.as-blockFull-width button

CSS Reference: Button Group

Segmented group of related buttons

Source: src/css/button.css

Classes

ClassDescription
.button-groupcomponent root