# IconButton

> Icon-only action button. Wraps @dt/Button in its rounded icon-only form with an enforced accessible label, sharing Button's variant (weight) x tone (semantic) matrix and surface awareness.

- Status: stable
- Group: actions
- Page: https://www.digitaltableteur.com/design-system/components/icon-button
- Contract: https://www.digitaltableteur.com/contracts/v1/IconButton.contract.json

## When to use

IconButton is an icon-only action for compact surfaces: toolbars, card corners, table rows, dialog close affordances. It wraps Button in its rounded icon-only form, so weight, tone, and surface behave exactly like Button; label is required and becomes the accessible name. A `className` is applied to an inline-flex wrapper span around the button, not to the button itself.

## When not to use

- Do not omit `label`; there is no fallback announcement.
- Do not use for primary marketing CTAs; text + icon is more discoverable; use `Button` with an icon prop.
- Do not rely on `tooltip` as the only accessible name.
- Do not use IconButton for primary marketing CTAs; text plus icon is more discoverable, use Button with an icon prop.

## Look-alikes

- Button: Do not use IconButton for primary marketing CTAs; text plus icon is more discoverable, use Button with an icon prop.

## Props

| Prop | Type | Default | Description | Required |
| --- | --- | --- | --- | --- |
| `icon` | `ReactNode \| string` | none | Icon glyph: a rendered element (e.g. <CaretRight />) or a Phosphor icon name (e.g. "x"). | Yes |
| `label` | `string` | none | Accessible name when `aria-labelledby` is not set. | Yes |
| `variant` | `"primary" \| "secondary" \| "tertiary"` | `tertiary` | Visual weight. | No |
| `tone` | `ButtonProps["tone"]` | `neutral` | Semantic colour, orthogonal to `variant`. | No |
| `surface` | `ButtonProps["surface"]` | `default` | Surface the control sits on; prefer over color overrides on tinted bands. | No |
| `size` | `"sm" \| "md" \| "lg"` | `md` | No description in the contract. | No |
| `tooltip` | `string` | none | Native tooltip text shown on hover; the `label` stays the accessible name. | No |

## Variants

- Available variants: `primary`, `secondary`, `tertiary` (default `tertiary`).
- Available tones: `neutral`, `error`, `warning`, `success`, `info` (default `neutral`).
- Available surfaces: `default`, `onDark`, `onBrand` (default `default`).
- Available sizes: `sm`, `md`, `lg` (default `md`).

## Invalid combinations

The contract records no invalid combinations.

## Keyboard

| Key | Behaviour |
| --- | --- |
| Tab | Focusable. |
| Enter / Space | Activates. |

## Rules

Every item under When not to use is a MUST NOT. MUST and SHOULD follow RFC 2119.

| Level | Rule | How it is checked |
| --- | --- | --- |
| MUST | ARIA: `label` prop is required and renders as the accessible name; the icon glyph itself is `aria-hidden`. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: `tooltip` is hover-only garnish; it never replaces `label` as the accessible name. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | ARIA: `disabled` (and Button's loading state) sets the native `disabled` attribute on the button, which removes it from the tab order and from pointer interaction; screen readers still expose it as a disabled button when browsing. It does not use `aria-disabled`. | Not yet verified: no evidence record. WCAG 4.1.2. |
| MUST | Required stories produce no axe violations at the configured severity. | Automated by `npm run test:stories:matrix:ci`. WCAG 2.5.8. |
| MUST | The accessibility tree (role, name, state) matches the committed snapshot for every required story. | Automated by `npm run test:stories:matrix:ci`. WCAG 1.3.1, 4.1.2. |
| MUST | The component remains legible and operable under a real browser forced-colors: active pass. | Automated by `npm run test:stories:hc`. |
| MUST | Every documented keyboard interaction works as specified. | Not yet verified: no evidence record. WCAG 2.1.1, 2.1.2. |
| MUST | A human completed an end-to-end accessibility review of this component. | Manual review, last on 2026-11-04. |
| SHOULD | Write label as the action, like Close dialog or Copy link, never the icon name like X or Clipboard. | Not stated in the contract. |
| SHOULD | Stay on the default variant=tertiary in toolbars and chrome; reserve primary for the one dominant action of a compact surface. | Not stated in the contract. |
| SHOULD | Pick an icon whose meaning survives without text (x, magnifying-glass, caret-*); if you have to explain the icon, use Button with a visible label instead. | Not stated in the contract. |
| SHOULD | Add tooltip for sighted-user reassurance on ambiguous glyphs; the label still carries the accessible name. | Not stated in the contract. |
| SHOULD | Use surface=onDark or onBrand when the control sits on a dark or brand band instead of overriding colors. | Not stated in the contract. |
| SHOULD NOT | Do not omit or auto-generate label; there is no fallback announcement and axe fails the render. | Not stated in the contract. |
| SHOULD NOT | Do not stack more than about five icon-only actions in a row without grouping or labels; recognition drops sharply. | Not stated in the contract. |

## Install and import

- Install: `npm install @digitaltableteur/react` (version 0.9.1, restricted access on npm).
- Import: `import { IconButton } from "@digitaltableteur/react";`
- Entry point: `@digitaltableteur/react/actions` exports it too.
- Styles: import `@digitaltableteur/react/style.css` once at the app boundary.
- Also available as the custom element `<dt-icon-button>`; see the custom elements manifest.

## Links

- Machine-readable contract (Design System Contract 1.0): https://www.digitaltableteur.com/contracts/v1/IconButton.contract.json
- Markdown version of this page: https://www.digitaltableteur.com/design-system/components/icon-button.md
- Component index as JSON: https://www.digitaltableteur.com/design-system/components.json
- All components: https://www.digitaltableteur.com/design-system/components
- Source: The implementation lives in a private repository. This page, its Markdown twin and the contract are generated from it at build time and are the public source of truth.
