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.
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 |
Variants, properties, states
Variants
Structurally different versions of the component.
primarysecondarytertiaryghostdestructive Properties
The same component, parameterised.
| Property | Type |
|---|---|
size | sm | md | lg |
iconOnly | boolean |
fullWidth | boolean |
type | button | submit | reset |
States
Browser/user-driven (interactive) vs. app-driven (data).
| Kind | States |
|---|---|
interactive | hoverfocus-visibleactivedisabled |
data | loadingpressed |
Cross-framework expression
| Framework | Structure mechanism | Variant 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` |
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.
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. |
Accessibility acceptance
Keyboard walk
| Keys | Expected |
|---|---|
Tab | Focus 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 Space | Activates 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
| Trigger | Expected |
|---|---|
| Focus enters the button | The 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 true | Screen 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 set | Native `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-namearia-allowed-rolecolor-contrastfocus-order-semantics
Common mistakes
#button-icon-only-no-label
Icon-only button with no `aria-label`
The visual is a single icon and the button carries no accessible name. Screen readers announce "button" with no indication of what it does.
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
The `disabled` attribute removes the button from the focus order, so keyboard users cannot trigger the tooltip and never see the explanation.
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
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.
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"`
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.
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
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.
Use the right element: `<a>` for navigation styled as a button, `<button>` for in-page actions. Never nest interactive elements.
Figma↔Code mismatches
- 01 Figma
A button drawn as a styled `<div>` (or "rectangle with text") with click-through interactions in the prototype
CodeA real `<button>` element with native focus, native key handling (Enter / Space), and form participation
ConsequenceDesigners 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.
CorrectDocument 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>`.
- 02 Figma
Hover / focus / active / disabled modeled as Figma variants on every variant × size combination
CodeCSS pseudo-classes (`:hover`, `:focus-visible`, `:active`) and the HTML `disabled` attribute
ConsequenceVariant 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.
CorrectStates 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.
- 03 Figma
A "loading button" variant that visually replaces the label with a spinner
CodeA `loading` data state that disables the button, swaps the leading icon for a spinner, and announces busy via `aria-busy="true"`
ConsequenceDesigners 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.
CorrectDocument `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.
- 04 Figma
A "link button" variant that looks like a button but navigates
CodeA `<button>` cannot navigate; navigation requires `<a>`. The "link button" must render as an anchor with button styling.
ConsequenceDevelopers 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.
CorrectDistinguish 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.