Dev view

Button

An interactive element that triggers an action when activated. Anchors most user-initiated state changes — submitting forms, opening dialogs, advancing flows. Distinct from a Link (which navigates) in semantic and visual treatment.

When to use

Use

When the user needs to perform an action that triggers a state change, submits a form, or initiates a flow. Default activator for in-page mutations.

Avoid

For navigating to another page or external URL — that is `Link`, even when styled like a button. For binary on/off state — that is `Switch`. For opening a menu of multiple options — that is `MenuButton`.

Versus related

  • link

    `Link` navigates (changes the URL or opens a new tab); `Button` performs an action without navigation. Visual styling may converge but semantics never do — middle-click, "open in new tab", and copy-as-URL all rely on `<a>`.

  • menu-button

    `MenuButton` opens a menu with multiple options and carries `aria-haspopup="menu"` plus `aria-expanded`. `Button` triggers a single action.

Highlight
Fig 1.1 · Button · Dev view
Dev

Code anatomy

Slot Code slot Semantic
root root button
icon-leading icon-leading presentational-or-img
label label text
icon-trailing icon-trailing presentational-or-img
Both

Variants, properties, states

Variants

Structurally different versions of the component.

primarysecondarytertiaryghostdestructive

Properties

The same component, parameterised.

PropertyType
sizesm | md | lg
iconOnlyboolean
fullWidthboolean
typebutton | submit | reset

States

Browser/user-driven (interactive) vs. app-driven (data).

KindStates
interactive
hoverfocus-visibleactivedisabled
data
loadingpressed
Dev

Cross-framework expression

FrameworkStructure mechanismVariant mechanism
Web Components A `<ui-button>` host element that internally renders a `<button>` and exposes named slots for `icon-leading`, default (label), and `icon-trailing` attributes (`variant="primary"`, `size="md"`, `loading`); reflected to host classes for CSS selectors
React a single `<Button>` component that accepts `leadingIcon`, `trailingIcon`, and children for the label props with class-variance-authority for variant / size / fullWidth; `data-loading` attribute for the loading state
Angular (signals) a `<ui-button>` component projecting the label via `<ng-content>`; `[uiIconLeading]` / `[uiIconTrailing]` directives or named projection slots input<'primary' | 'secondary' | 'tertiary' | 'ghost' | 'destructive'>(); input<'sm' | 'md' | 'lg'>(); host-bound `[disabled]` and `[attr.aria-busy]`
Vue a `<Button>` SFC with named slots for leading and trailing icons and a default slot for the label defineProps with literal-union types; `:loading` boolean prop drives `aria-busy`
Dev

Form integration

name attribute
`<button>` with a `name` attribute participates in form submission — the button's `[name]=[value]` is appended to the form's FormData when this button is the submitter. Buttons without `name` do not contribute to FormData. The canonical reference treats `name` as a passthrough to the underlying `<button>`.
FormData serialization
On submit, only the activating submit button's `[name]=[value]` pair is appended (not all buttons in the form). This makes Button useful for multi-action forms — `<button name="action" value="save">` and `<button name="action" value="discard">` distinguish via the submitted value of the same key.
form.reset()
`<button type="reset">` calls `form.reset()` on the parent form, restoring controls to their default values and clearing any `setCustomValidity()` state. Programmatic `form.reset()` triggers the same path. Reset does not submit and does not navigate.
HTML5 validation
`<button type="submit">` triggers HTML5 validation (`form.checkValidity()`); the first invalid field receives focus and the form's `:invalid` pseudo-class state propagates. The `formnovalidate` attribute on the submit button suppresses validation for that specific submit path — useful for "save draft" patterns where partial input is allowed.
Both

Accessibility

Slot Accessibility hint
root Always render as `<button type="button">` (or `type="submit"` inside a form). Do not use `<div role="button">` — it loses the implicit form participation and key handling. Disabled state via the HTML `disabled` attribute (not `aria-disabled`) for buttons that should not receive focus; use `aria-disabled` only when the disabled button must remain focusable for assistive-tech announcement.
icon-leading Decorative when paired with a visible text label — `aria-hidden="true"` on the icon, no alt. If the icon is the sole label of the button (icon-only variant), the button carries an `aria-label` describing the action.
label Plain text node, no special role. The label is the button's accessible name unless the button has an `aria-label` or `aria-labelledby` override. Avoid using only an icon as the label without an `aria-label`.
icon-trailing Decorative; `aria-hidden="true"`. If the icon communicates state ("opens menu", "external link"), pair it with an invisible-text addition to the label or an `aria-label` that captures the meaning.
Both

Accessibility acceptance

Keyboard walk

KeysExpected
TabFocus enters the button when reachable in tab order. The visible focus ring matches `color.border.focus`; non-disabled buttons are always reachable, `disabled` buttons are skipped.
Enter or SpaceActivates the button (same handler path as a pointer click). For Space, activation fires on key release, not on key down — matches the native `<button>` behavior.

Screen-reader announcements

TriggerExpected
Focus enters the buttonThe button's accessible name followed by "button" (e.g. "Save changes, button"). Icon-only buttons announce the `aria-label` in place of the missing visible label.
`loading` data state set to trueScreen readers announce the busy state via `aria-busy="true"`. The accessible name remains the original label ("Save changes, busy, button") rather than being replaced with "loading".
Disabled state setNative `disabled` removes the button from focus order silently. `aria-disabled="true"` keeps it focusable; SR announces "<label>, dimmed, button" or equivalent (varies by SR vendor).

axe-core rules to assert

  • button-name
  • aria-allowed-role
  • color-contrast
  • focus-order-semantics
Dev

Common mistakes

#button-icon-only-no-label

Icon-only button with no `aria-label`

Problem

The visual is a single icon and the button carries no accessible name. Screen readers announce "button" with no indication of what it does.

Fix

Every icon-only button has an `aria-label` describing the action ("Close", "Sort ascending"). Lint rule: any button whose textContent is empty must have `aria-label` or `aria-labelledby`.

#button-disabled-tooltip

Disabled button has a tooltip explaining why

Problem

The `disabled` attribute removes the button from the focus order, so keyboard users cannot trigger the tooltip and never see the explanation.

Fix

Use `aria-disabled="true"` instead of `disabled` when the button needs to remain focusable for tooltip discovery. Activation is suppressed via a click-handler guard. The tooltip appears on focus and hover.

#button-loading-removes-label

Loading state replaces the label with a spinner only

Problem

The visible label is replaced by a spinner, but the button's accessible name remains the original label. Screen-reader users hear "Save" while the button is in fact unactivatable.

Fix

Keep the label visible (or visually hidden but accessible) during the loading state. Set `aria-busy="true"`. Announce the loading start and completion via a live region rather than mutating the button's accessible name.

#button-submit-default-in-toolbar

A button inside a `<form>` defaults to `type="submit"`

Problem

A button placed inside a form without an explicit `type` attribute submits the form when clicked. The first button in a toolbar accidentally becomes the form's submit on Enter.

Fix

Always set `type="button"` on buttons that do not submit. Make the canonical reference's default `type` value `"button"`, and require an explicit `type="submit"` opt-in.

#button-text-in-anchor

A `<button>` wrapped in an `<a>` for navigation

Problem

The DOM has a button inside an anchor (`<a><button>Go</button></a>`), producing nested interactive elements. Assistive tech announces ambiguously, and click events bubble through both.

Fix

Use the right element: `<a>` for navigation styled as a button, `<button>` for in-page actions. Never nest interactive elements.

Figma↔Code mismatches
  1. 01
    Figma

    A button drawn as a styled `<div>` (or "rectangle with text") with click-through interactions in the prototype

    Code

    A real `<button>` element with native focus, native key handling (Enter / Space), and form participation

    Consequence

    Designers prototype clickable rectangles that work with mouse input but break for keyboard users when implemented literally. Developers shipping divs lose form submission and have to reimplement keyboard activation manually.

    Correct

    Document the button as a semantic element in the canonical reference. Designers may draw rectangles in Figma but the component instance is named "Button" and developers always render it as `<button>`.

  2. 02
    Figma

    Hover / focus / active / disabled modeled as Figma variants on every variant × size combination

    Code

    CSS pseudo-classes (`:hover`, `:focus-visible`, `:active`) and the HTML `disabled` attribute

    Consequence

    Variant explosion: 5 variants × 3 sizes × 4 states × 2 widths = 120 component variants. The Figma file becomes unmaintainable and developers cannot mechanically map a Figma "hover variant" to a CSS rule.

    Correct

    States live on a separate states sheet, documented once. Figma variants are reserved for variant (primary / secondary / …), size, and width (auto / full). State styling is documented as CSS rules referencing `:hover`, `:focus-visible`, etc.

  3. 03
    Figma

    A "loading button" variant that visually replaces the label with a spinner

    Code

    A `loading` data state that disables the button, swaps the leading icon for a spinner, and announces busy via `aria-busy="true"`

    Consequence

    Designers may not realise the button must remain announced as busy; developers may strip the disabled attribute to keep the button focusable, missing the announcement entirely.

    Correct

    Document `loading` as a data state. The button is `aria-busy`, not `disabled` (so its label remains announceable), and a spinner replaces the leading icon. The button is non-activatable while loading via a click-handler guard, not via the disabled attribute.

  4. 04
    Figma

    A "link button" variant that looks like a button but navigates

    Code

    A `<button>` cannot navigate; navigation requires `<a>`. The "link button" must render as an anchor with button styling.

    Consequence

    Developers ship a `<button>` with a click handler that calls `router.push`. The element loses middle-click "open in new tab", cannot be copied as a URL, and breaks user expectations for links.

    Correct

    Distinguish at the canonical level: a `Button` triggers an action, a `Link` navigates. A "link styled as a button" remains an `<a>` element with button visual treatment. The component reference may document a `Link` component that shares the button's visual variants.