<!-- Protocore Design System — IconButton -->
# IconButton

- **Category:** Inputs (`inputs`)
- **Slug:** `inputs/icon-button`
- **Status:** stable
- **Platforms:** web, mobile
- **Import:** `import { IconButton } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/inputs/icon-button

> A square, icon-only action control — Button's affordances with a required aria-label.

## When to use

Use **IconButton** when the action is well-understood from its glyph alone and space is tight — toolbars, table row actions, the close affordance on a panel.

- If the meaning isn't unmistakable from the icon, use a text **Button** (with a `startIcon` if you like) instead — an ambiguous glyph is worse than a word.
- Always supply an `aria-label`, and consider pairing the control with a **Tooltip** so sighted users get the same label on hover.
- For a menu of actions behind one trigger, wrap an IconButton with **DropdownMenu**.

## Mobile (React Native)

**Preview.** `@protocore/pds-mobile` ships the React Native sibling of **IconButton**. It mirrors the web API where React Native allows; the package is a **preview** with no device-level QA yet, so pin it and expect small changes.

Import it from the mobile package (not `@protocore/pds`), inside a `<PdsProvider>` — there is no stylesheet, so `style` (a `ViewStyle`) replaces `className` and every value comes from the theme:

```tsx
import { IconButton } from "@protocore/pds-mobile";
```

**Parity with web.** Same interaction model as the mobile Button — square, icon-only, `Pressable`-based.

- `accessibilityLabel` is **required** (typed non-optional) — it replaces web's `aria-label` for the icon-only control.
- The icon is passed as `children`; press inverts/fills exactly like Button.
- `onPress` replaces `onClick`.

```tsx
<IconButton accessibilityLabel="Refresh" onPress={reload}>
  <RefreshIcon />
</IconButton>
```

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `aria-label` | `string` | yes | — | Accessible name for the control. Required — an icon-only button has no visible text. |
| `asChild` | `boolean` | no | `false` | Render as the single child element (Radix `Slot`) instead of a `<button>`. |
| `className` | `string` | no | — | — |
| `loading` | `boolean` | no | `false` | Show an inline spinner, set `aria-busy`, and disable the control while a background action runs. |
| `size` | `enum` | no | `md` | Square control size: sm 32 · md 36 · lg 40 · xl 48. |
| `style` | `CSSProperties` | no | — | — |
| `variant` | `enum` | no | `secondary` | Visual weight, matching Button: `primary` inverts on hover; `secondary`/`ghost` are quieter; `danger` fills on hover. |

## Examples

### Basics

A compact, square button that carries a single icon and nothing else. Because there is no visible text, `aria-label` is a **required** prop — TypeScript will flag its absence.

```tsx
import { IconButton } from "@protocore/pds";
import { RefreshCw } from "lucide-react";

export default function Basics() {
  // aria-label is required — the icon carries no visible text.
  return (
    <IconButton aria-label="Refresh metrics">
      <RefreshCw size={16} />
    </IconButton>
  );
}
```

### Variants

The same four weights as Button. IconButton defaults to `secondary` (a quiet outline) rather than `primary`, since icon-only actions are usually secondary controls.

```tsx
import { IconButton } from "@protocore/pds";
import { Pause, Play, Settings, Trash2 } from "lucide-react";

export default function Variants() {
  return (
    <div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
      <IconButton variant="primary" aria-label="Resume stream">
        <Play size={16} />
      </IconButton>
      <IconButton variant="secondary" aria-label="Pause stream">
        <Pause size={16} />
      </IconButton>
      <IconButton variant="ghost" aria-label="Settings">
        <Settings size={16} />
      </IconButton>
      <IconButton variant="danger" aria-label="Delete node">
        <Trash2 size={16} />
      </IconButton>
    </div>
  );
}
```

## Do & don't

**Do**

- Give every IconButton a concise, verb-first aria-label ("Delete node", not "Trash").
- Use it for universally-legible glyphs — close, refresh, play, more.
- Pair it with a Tooltip so the label is visible on hover as well as to screen readers.
- Keep icon and button sizes proportional so the glyph stays centred.

**Don't**

- Don't ship an IconButton without an aria-label — the control is then unnamed.
- Don't use an obscure icon to save a few pixels; prefer a text Button when meaning is unclear.
- Don't put a text label inside — that's a Button with a startIcon.
- Don't reuse the same generic label ("Button") across several actions.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Tab / Shift+Tab` | Move focus onto / off the button. |
| `Space or Enter` | Activate the button. |

**Notes**

- `aria-label` is required by the type — it becomes the control's accessible name.
- The icon is rendered `aria-hidden`; the label alone names the control.
- `loading` sets `aria-busy` and disables the control while a background action runs.
- Inherits Button's variants and CSS interaction states (no React hover).

## Related

`button`, `button-group`, `tooltip`, `dropdown-menu`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
