# Button

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

- Group: Actions
- Import: `import { Button } from '@misoto22/design'`
- Page: https://ui.misoto22.com/components/button/
- Related: floating-icon-button, spinner

## When to reach for it

Anything that DOES something. If it navigates and looks like text, it is a link, not a ghost button.

## Anatomy

- **Control box** (required) — The <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.
- **Label** — children, and the accessible name of a text button. It stays put while loading, so the width does not move under the pointer.
- **Icon** — The 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.
- **Keycap** — keycap, after the label — a mono glyph in a bordered box at reduced opacity. Real text, not decoration hidden from assistive tech.
- **Spinner** — loading, 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.

## 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.

## Keyboard

- Enter — Activates the button.
- Space — Activates the button. A native <button> answers to both; a styled <div> answers to neither.

## Button

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.

### Props

- `children` — `ReactNode`.
- `variant` — `ButtonVariant` default `'primary'`. Which action this is. `primary` is the one thing the screen wants you to do, so there is at most one per view; `danger` is reserved for destructive actions and is the only place chroma is allowed on a control.
- `size` — `ButtonSize` default `'md'`. 36 / 44 / 48px tall. `md` meets the pointer-target floor on its own.
- `keycap` — `string`. Optional mono keycap glyph rendered after the label (e.g. "P").
- `loading` — `boolean` default `false`. Swaps the leading content for a spinner and blocks interaction. The label stays put — a button that empties out while it works loses its width, and the page reflows under the pointer that just clicked it.
- `iconOnly` — `boolean` default `false`. Square control with no label. REQUIRES `aria-label`: an icon-only button with no accessible name is invisible to a screen reader, and this is the single most common way a design system ships an unusable control.
- `asChild` — `boolean` default `false`. Render the child element instead of a `<button>`, keeping these styles. Use it to hand the styling to a router's `Link` — `asChild` is what keeps this package free of any one router. The decoration slots (`keycap`, `loading`) are not injected into a slotted child; compose them inside it yourself.
- `className` — `string`.
- `href` — `string`.

Also accepts: `AnchorHTMLAttributes<HTMLAnchorElement>`, `ButtonHTMLAttributes<HTMLButtonElement>`.

## Example — variants

```tsx
import { Button } from '@misoto22/design'

<div className="flex flex-wrap items-center gap-3">
  <Button>Primary</Button>
  <Button variant="secondary">Secondary</Button>
  <Button variant="ghost">Ghost</Button>
  <Button variant="danger">Delete</Button>
</div>
```

## Example — sizes

```tsx
import { Button } from '@misoto22/design'

<div className="flex flex-wrap items-center gap-3">
  <Button size="sm" variant="secondary">Small</Button>
  <Button size="md" variant="secondary">Medium</Button>
  <Button size="lg" variant="secondary">Large</Button>
</div>
```

## Example — states

```tsx
import { Button } from '@misoto22/design'

<div className="flex flex-wrap items-center gap-3">
  <Button loading>Saving…</Button>
  <Button disabled>Disabled</Button>
  <Button variant="secondary" keycap="P">View projects</Button>
</div>
```

## Example — icon only

```tsx
import { Button } from '@misoto22/design'

<div className="flex flex-wrap items-center gap-3">
  <Button iconOnly aria-label="Copy to clipboard" variant="secondary">
    <RiFileCopyLine size={16} aria-hidden />
  </Button>
  <Button iconOnly aria-label="Share" variant="ghost">
    <RiShareLine size={16} aria-hidden />
  </Button>
  <Button iconOnly aria-label="Open in a new tab">
    <RiArrowRightUpLine size={16} aria-hidden />
  </Button>
</div>
```
