Skip to content
Folio

Actions

Button

The system’s action, on the same corner as the field beside it.

Usage

When to reach for it

Anything that DOES something. If it navigates and looks like text, it is a link, not a ghost button.
TSX
import { Button } from '@misoto22/design'

Notes

The system's action.

Renders a <button>, or an <a> when given href, or whatever you hand it via asChild. No router is imported, so the package stays framework-agnostic and a Next or React Router app wires its own Link at the call site.

Server-component friendly: there is no client boundary here, so it renders in a static page as well as an interactive one.

Anatomy

Button anatomy
ElementDescription
Control boxrequiredThe <button>, the <a> it becomes when given href, or whatever asChild slots in. It carries the variant, the size, and the same --radius corner as the field beside it.
Labelchildren, and the accessible name of a text button. It stays put while loading, so the width does not move under the pointer.
IconThe same children slot on an iconOnly button: the box goes square, the padding to zero, and nothing text-shaped is left behind for a screen reader.
Keycapkeycap, after the label — a mono glyph in a bordered box at reduced opacity. Real text, not decoration hidden from assistive tech.
Spinnerloading, before the label — a Spinner toned to the ground it sits on and passed label={null}, so it is aria-hidden and aria-busy on the control carries the state instead.

Best practices

Do

  • Spell out type="submit" on a form’s submit control: the default here is type="button", so the button at the foot of a form looks right and submits nothing.
  • Keep one primary to a view. variant defaults to primary, so a row written without the prop is a row in which every button claims to be the one thing the screen wants.
  • Reach for loading rather than swapping the label by hand: it holds the label, sets aria-busy and disables the control in one move, so the box does not collapse under the pointer that just clicked it.
  • Use href when it navigates and asChild when a router owns the navigation — a <button> whose onClick calls router.push cannot be opened in a new tab, and is announced as a button that goes nowhere.

Don’t

  • asChild passes the styling to the child and nothing else: keycap and loading never reach it, so a loading state written that way shows no spinner and blocks no clicks.
  • sm is 36px at the default density, under the 44px md clears on its own — a toolbar built out of sm is a row of targets a thumb misses (WCAG 2.5.5).
  • danger is a state, not emphasis. Spent on the merely important action, nothing is left that reads as destructive when one actually is.
  • The keycap is not hidden from assistive tech, so the glyph joins the accessible name — the control is read out as “Save S”, and a keycap for a shortcut nothing binds announces a promise the page never keeps.

Examples

variants

The four variants, in the order they compete for attention. variant defaults to primary, so keep one to a view and give every other control secondary or ghost; danger is a state rather than emphasis, and spending it on the merely important leaves nothing that reads as destructive when something is.

sizes

The three sizes on one variant, so the box is the only thing changing. Reach for md nearly always: sm is 36px at the default density, under the 44px md clears on its own, so a toolbar built out of it is a row of targets a thumb misses (WCAG 2.5.5) — and lg is for the one action a page is actually about.

states

A control in flight, a control that is off, and a control advertising a shortcut. Reach for loading rather than swapping the label by hand: it holds the label, sets aria-busy and blocks the click in one move, so the box does not collapse under the pointer that just hit it. The keycap is real text and joins the accessible name, so write one only where a key is actually bound.

icon only

A square control carrying a glyph and no label, for a toolbar where the word would only repeat what the row already says. iconOnly leaves nothing text-shaped behind, so aria-label is not optional here — an icon button without one is announced as an unnamed button, and is the single most common way a design system ships a control nobody can use.

Types

TSX
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger'
export type ButtonSize = 'sm' | 'md' | 'lg'

Keyboard

Button keyboard interactions
KeyDoes
EnterActivates the button.
SpaceActivates the button. A native <button> answers to both; a styled <div> answers to neither.

Accessibility

  • A native <button> by default, so Enter and Space both fire it.
  • loading sets aria-busy and disables the control; the label stays, so the box does not collapse under the pointer that just clicked it.
  • A link cannot be disabled, so href + loading sets aria-disabled and blocks pointer events instead.
  • iconOnly has no text, so it requires aria-label — the single most common way a design system ships an unusable control.