Designer 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>`.
- icon-button
`IconButton` is the same component family rendered with `iconOnly: true` plus a required `aria-label`. Document as a configuration of `Button`, not a separate component.
- menu-button
`MenuButton` opens a menu with multiple options and carries `aria-haspopup="menu"` plus `aria-expanded`. `Button` triggers a single action.
Figma anatomy
| Slot | Figma type | Hint |
|---|---|---|
root | frame | Auto-layout horizontal frame; min-height per size token; padding from variant set |
icon-leading | instance | Icon component instance; size bound to button size token |
label | text | Button label text style; bound to a "label" component property |
icon-trailing | instance | Icon component instance; visibility bound to "has trailing icon" property |
Token usage per slot
root- spacing
- padding
spacing.compact - gap
spacing.tight
- padding
- radius
- corner
radius.md
- corner
- color
- background
color.accent.bg - foreground
color.accent.fg - border
color.border.subtle - ring
color.border.focus
- background
- elevation
- shadow
elevation.sm
- shadow
- typography
- size
text.md - weight
weight.semibold
- size
icon-leading- color
- foreground
color.accent.fg
- foreground
label- color
- foreground
color.accent.fg
- foreground
- typography
- size
text.md - weight
weight.semibold - tracking
tracking.normal
- size
icon-trailing- color
- foreground
color.accent.fg
- foreground
Figma ↔ Code property map
| Figma | Type | Code | Notes |
|---|---|---|---|
Variant | Variant | variant | Maps the visual variant set (primary / secondary / tertiary / ghost / destructive). Figma stores it as a Variant property; code as a string union. |
Size | Variant | size | — |
Has Leading Icon | Boolean | iconLeading | Toggles slot visibility in Figma. Code does not have a matching boolean — the icon-leading slot is conditionally rendered based on whether a child is provided. |
Has Trailing Icon | Boolean | iconTrailing | — |
Leading Icon | Instance Swap | iconLeading | Swaps the icon component instance when 'Has Leading Icon' is true. Code passes the icon as slot child (React `leadingIcon` prop or named slot). |
Label | Text | children | Maps to the default slot / children — the label text. |
Loading | Boolean | loading | Drives the data state. In Figma it toggles the spinner overlay; in code it sets `aria-busy="true"` and replaces the leading icon with a spinner. |
Full Width | Boolean | fullWidth | — |
Internationalisation
RTL · mirroring
Leading and trailing icons swap visual position via logical `inline-start` / `inline-end` properties — what was on the left in LTR appears on the right in RTL. Directional icons (chevrons, arrows indicating progression) flip horizontally to preserve their semantic direction. Loading-spinner rotation does *not* mirror — circular motion is direction-neutral. The focus ring, padding, and typography render identically in both directions.
Text expansion
Labels can grow 30–50% longer in German, Russian, or Finnish. The canonical Button does not enforce a max-width — buttons grow with their label. Truncation is a last resort and requires a `title` attribute carrying the full label so SR users still hear it. Icon-only buttons are immune (the icon does not translate); the aria-label is the translation surface.
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 |
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.
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.
Accessibility hints
| 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. |