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.

Highlight
Fig 1.1 · Button · Designer view
Designer

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
Designer

Token usage per slot

root
spacing
  • paddingspacing.compact
  • gapspacing.tight
radius
  • cornerradius.md
color
  • backgroundcolor.accent.bg
  • foregroundcolor.accent.fg
  • bordercolor.border.subtle
  • ringcolor.border.focus
elevation
  • shadowelevation.sm
typography
  • sizetext.md
  • weightweight.semibold
icon-leading
color
  • foregroundcolor.accent.fg
label
color
  • foregroundcolor.accent.fg
typography
  • sizetext.md
  • weightweight.semibold
  • trackingtracking.normal
icon-trailing
color
  • foregroundcolor.accent.fg
Both

Figma ↔ Code property map

FigmaTypeCodeNotes
VariantVariantvariantMaps the visual variant set (primary / secondary / tertiary / ghost / destructive). Figma stores it as a Variant property; code as a string union.
SizeVariantsize
Has Leading IconBooleaniconLeadingToggles 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 IconBooleaniconTrailing
Leading IconInstance SwapiconLeadingSwaps the icon component instance when 'Has Leading Icon' is true. Code passes the icon as slot child (React `leadingIcon` prop or named slot).
LabelTextchildrenMaps to the default slot / children — the label text.
LoadingBooleanloadingDrives 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 WidthBooleanfullWidth
Both

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.

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
Both

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.

Designer

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.

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.