Button
Triggers an action with a side effect. Pure navigation is a link (`<a>`, router link) — even when it should look like a button.
Added 14 October 2026
Button or link?
Decide by side effect, not by looks:
- The click has a side effect (submit, persist, mutate state) — even if it also
navigates afterwards →
Button. - The click only navigates → a real link, styled with
buttonVariants(...)when it must look like a button. Middle-click, open-in-new-tab and screen readers depend on this. [a11y] - Carve-out: an icon-only “back” control in a flow may be a
ghostButton for hit-target reasons;aria-labelstill mandatory.
Variants — which one and when
Naming warning: default means primary, not “ordinary button” — the name is
inherited from shadcn and we keep it for ecosystem compatibility. An omitted variant
also renders primary. So never reach for a bare <Button> as a neutral action: a
neutral button is secondary or outline, and default must pass the one-per-surface
rule below.
default— the one primary action of the current surface: a card, dialog, form or self-contained section — not the whole page. At most one visible per surface; a grid of cards legitimately shows one per card. See docs/patterns/action-hierarchy.md.secondary— a stand-alone neutral action: card-body actions (“Continue editing”), toolbar controls, the “Back” companion next to a primary in a form row.outline— a utility action attached to a context: reset-to-default, inline-edit save/cancel icon pairs, remediation buttons inside notices; also the demoted second action when the same card already shows adefault.ghost— lowest emphasis: dismiss/close/back, toolbar icon actions, repeated row actions in dense lists.destructive— removing or revoking something; item-scoped (“remove this file”, “remove this site”). Confirmation is required only when the action is irreversible or costly — see docs/patterns/action-hierarchy.md.floating— a control laid over a map (zoom, locate, fit all, expand): popover surface, no edge, its own shadow. Never on a regular page surface — there it is asecondaryoroutline. Stacks go in aButtonGroup, which takes over the shadow and draws the dividers. See docs/patterns/map-controls.md.link— an action that must read as inline text. Not for navigation — that is a real link. Currently unused in consumers; prefer the variants above.
Sizes — which one and when
default— forms, dialogs, most surfaces. Use it unless a rule below applies.sm— companion actions inside cards and notices.xs— extra-dense surfaces (tags, tight rows).lg— prominent full-width section actions.cta— the flow-defining forward action of the whole journey: advance the wizard, create the first campaign, pay. At most one visible per screen, always withvariant="default", full-width when the container is a narrow stacked column. Never rebuild it with custom classes — use the size.icon,icon-lg,icon-sm,icon-xs— icon-only;aria-labelis mandatory [a11y]. Context guide from real usage:iconfor toolbars and map/media controls,icon-smfor inline edit and dialog close,icon-xsfor in-card dismiss.
States
- Disabled — never the only explanation. Show why: a tooltip listing the blockers (the wizard-continue pattern) or an adjacent hint. Gate with “disabled with reason”; don’t hide the control. [research]
- Busy — while an async action runs: disable, swap the label to a progressive form (“Sending…”, “Processing…”), add a spinner when the wait is noticeable.
Never
- Two
defaultbuttons visible at once on the same surface. size="cta"more than once per screen, with a variant other thandefault, or hand-faked viaclassName.- An icon-only button without
aria-label. [a11y] - A
disabledbutton with no visible reason why. - A Button whose click handler only navigates — use a link.
Related
- docs/patterns/action-hierarchy.md — arranging multiple actions on one surface.
- docs/patterns/copy.md — what the label says: sentence case, verb + object.
- docs/patterns/forms.md — submit placement, validation, busy states.