# IconButton

> Package `@umriss-ui/core`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/core/iconbutton/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/core/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/core/llms-full.txt).

A square button that shows only an icon (also called an icon-only button): refresh, export, close, the actions of a row. Reach for it where the icon is known well enough to stand alone; its name is required and shows as a tooltip.

```ts
import { IconButton } from "@umriss-ui/core";
```

Demo page: https://romanhaendler.github.io/umriss-ui/core/iconbutton/

Its `aria-label` is required, and it is the accessible name and the tooltip in one: a screen reader hears it once, and the eye reads it on hover or keyboard focus.

The icon is yours: an SVG, a glyph of the set, or an icon font's element. The button sets its size – 14 px, 12 px at `sm` – the size an SVG takes beside a label in a `Button` as well.

It is a `Button` underneath, with the same sizes, loading state and variants; the default is `plain`, quiet and neutral. As the trigger of a `Menu` it anchors the panel, and its tooltip gives way while the menu is open.

## Examples

### Name the icon

`aria-label` is required: it is the name a screen reader announces and the tooltip a sighted reader sees on hover or keyboard focus.

```tsx
import { IconButton } from "@umriss-ui/core";

function Refresh() {
  return (
    <svg viewBox="0 0 10 10" aria-hidden="true">
      <path d="M8.3 5.8A3.4 3.4 0 1 1 7.6 2.7M7.9 1.2v1.8H6.1" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

export default function NameTheIcon() {
  return (
    <IconButton aria-label="Refresh the tours">
      <Refresh />
    </IconButton>
  );
}
```

### Variants

`plain` is the default – quiet and neutral, for a bar or a row. `ghost` is quiet in the accent; the others mean what they mean on a `Button`.

```tsx
import { CrossGlyph, IconButton, PlusGlyph, Stack } from "@umriss-ui/core";

function Export() {
  return (
    <svg viewBox="0 0 10 10" aria-hidden="true">
      <path d="M5 1.5v5M2.8 4.3 5 6.5l2.2-2.2M1.8 8.5h6.4" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

function Check() {
  return (
    <svg viewBox="0 0 10 10" aria-hidden="true">
      <path d="M1.8 5.3 4 7.5l4.2-5" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

export default function Variants() {
  return (
    <Stack direction="row" gap={3} wrap align="center">
      <IconButton aria-label="Export the tour list">
        <Export />
      </IconButton>
      <IconButton variant="ghost" aria-label="Add a stop">
        <PlusGlyph />
      </IconButton>
      <IconButton variant="secondary" aria-label="Export the tour list">
        <Export />
      </IconButton>
      <IconButton variant="primary" aria-label="Release the tour">
        <Check />
      </IconButton>
      <IconButton variant="danger" aria-label="Cancel the tour">
        <CrossGlyph />
      </IconButton>
    </Stack>
  );
}
```

### Sizes

The heights of `Button`, and the same icon size beside a label and alone: 14 px at `md`, 12 px at `sm`, in header bars and table rows.

```tsx
import { Button, IconButton, Stack } from "@umriss-ui/core";

function Export() {
  return (
    <svg viewBox="0 0 10 10" aria-hidden="true">
      <path d="M5 1.5v5M2.8 4.3 5 6.5l2.2-2.2M1.8 8.5h6.4" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

export default function Sizes() {
  return (
    <Stack gap={3}>
      <Stack direction="row" gap={2} align="center">
        <Button>
          <Export />
          Export
        </Button>
        <IconButton variant="secondary" aria-label="Export the tour list">
          <Export />
        </IconButton>
      </Stack>
      <Stack direction="row" gap={2} align="center">
        <Button size="sm">
          <Export />
          Export
        </Button>
        <IconButton size="sm" variant="secondary" aria-label="Export the tour list">
          <Export />
        </IconButton>
      </Stack>
    </Stack>
  );
}
```

### Bring your own icons

A glyph of the set, an SVG drawn for 24 px as lucide and Font Awesome ship theirs, a character the way an icon font sets it – the button gives each the same size.

```tsx
import { CrossGlyph, IconButton, Stack } from "@umriss-ui/core";

/* Drawn on a 24 grid with its own width and height, as icon sets ship them. */
function Printer() {
  return (
    <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <path d="M6 9V2h12v7" />
      <path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2" />
      <path d="M6 14h12v8H6z" />
    </svg>
  );
}

export default function BringYourOwnIcons() {
  return (
    <Stack direction="row" gap={3} align="center">
      <IconButton variant="secondary" aria-label="Remove the filter">
        <CrossGlyph />
      </IconButton>
      <IconButton variant="secondary" aria-label="Print the delivery notes">
        <Printer />
      </IconButton>
      <IconButton variant="secondary" aria-label="Sort by departure">
        <span aria-hidden="true">⇅</span>
      </IconButton>
    </Stack>
  );
}
```

### Loading and disabled

While its action runs, the spinner takes the icon's place and the button locks; disabled, it fades and keeps its name.

```tsx
import { useEffect, useState } from "react";
import { IconButton, Stack, Text } from "@umriss-ui/core";

function Refresh() {
  return (
    <svg viewBox="0 0 10 10" aria-hidden="true">
      <path d="M8.3 5.8A3.4 3.4 0 1 1 7.6 2.7M7.9 1.2v1.8H6.1" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

function Export() {
  return (
    <svg viewBox="0 0 10 10" aria-hidden="true">
      <path d="M5 1.5v5M2.8 4.3 5 6.5l2.2-2.2M1.8 8.5h6.4" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

export default function LoadingAndDisabled() {
  const [loading, setLoading] = useState(false);
  const [refreshed, setRefreshed] = useState("10:30");

  useEffect(() => {
    if (!loading) return;
    const timer = setTimeout(() => {
      setLoading(false);
      setRefreshed("10:31");
    }, 1500);
    return () => clearTimeout(timer);
  }, [loading]);

  return (
    <Stack direction="row" gap={2} align="center">
      <IconButton variant="secondary" aria-label="Refresh the tours" loading={loading} onClick={() => setLoading(true)}>
        <Refresh />
      </IconButton>
      <IconButton variant="secondary" aria-label="Export the tour list" disabled>
        <Export />
      </IconButton>
      <Text size="sm" tone="muted">
        Refreshed at {refreshed}
      </Text>
    </Stack>
  );
}
```

### Build a toolbar

Plain buttons for the bar, a group for the pair that belongs together, a menu for the rest – every square the same size, every name a tooltip.

```tsx
import { useState } from "react";
import { ButtonGroup, IconButton, Menu, MenuItem, MenuSeparator, MinusGlyph, PlusGlyph, Stack, Text } from "@umriss-ui/core";

const ZOOM = [50, 75, 100, 150, 200];

function Refresh() {
  return (
    <svg viewBox="0 0 10 10" aria-hidden="true">
      <path d="M8.3 5.8A3.4 3.4 0 1 1 7.6 2.7M7.9 1.2v1.8H6.1" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

function Export() {
  return (
    <svg viewBox="0 0 10 10" aria-hidden="true">
      <path d="M5 1.5v5M2.8 4.3 5 6.5l2.2-2.2M1.8 8.5h6.4" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

export default function Toolbar() {
  const [zoom, setZoom] = useState(2);

  return (
    <Stack direction="row" gap={3} align="center" justify="space-between" wrap style={{ maxWidth: 620 }}>
      <Text size="sm" weight="semibold">
        Tours, Tuesday 17 March
      </Text>
      <Stack direction="row" gap={2} align="center">
        <ButtonGroup aria-label="Zoom the tour plan">
          <IconButton size="sm" variant="secondary" aria-label="Zoom out" disabled={zoom === 0} onClick={() => setZoom(zoom - 1)}>
            <MinusGlyph />
          </IconButton>
          <IconButton size="sm" variant="secondary" aria-label="Zoom in" disabled={zoom === ZOOM.length - 1} onClick={() => setZoom(zoom + 1)}>
            <PlusGlyph />
          </IconButton>
        </ButtonGroup>
        <Text as="span" size="xs" mono tone="muted" style={{ width: "4ch" }}>
          {ZOOM[zoom]}%
        </Text>
        <IconButton size="sm" aria-label="Refresh the tours">
          <Refresh />
        </IconButton>
        <IconButton size="sm" aria-label="Export the tour list">
          <Export />
        </IconButton>
        <Menu
          align="end"
          trigger={
            <IconButton size="sm" aria-label="More actions for the tours">
              ⋯
            </IconButton>
          }
        >
          <MenuItem>Print the delivery notes</MenuItem>
          <MenuItem>Copy the link</MenuItem>
          <MenuSeparator />
          <MenuItem tone="danger">Cancel every open tour</MenuItem>
        </Menu>
      </Stack>
    </Stack>
  );
}
```

## When to use something else

- The action is clearer said than drawn → Button
- Icon buttons that act on the same thing → ButtonGroup

## Keyboard

| Key | Action |
|---|---|
| `Tab` | Reaches the button and shows its name. |
| `Enter / Space` | Presses it. |
| `Escape` | Hides the name. |

## API

### `IconButtonProps`

| Prop | Type | Default | Description |
|---|---|---|---|
| `children` *required* | `ReactNode` | — | The icon: an SVG, a glyph of the set or an icon font's element. The button sets its size - `--u-icon-size`, `--u-icon-size-sm` at `sm`.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/iconbutton/#configurator-iconbutton), [Name the icon](https://romanhaendler.github.io/umriss-ui/core/iconbutton/#name-the-icon), [Variants](https://romanhaendler.github.io/umriss-ui/core/iconbutton/#variants) and 5 more |
| `variant` | [`ButtonVariant`](https://romanhaendler.github.io/umriss-ui/core/iconbutton/#type-ButtonVariant)<br>`"primary" \| "secondary" \| "ghost" \| "plain" \| "danger"` | `"plain"` | How loud the button is; `plain` is quiet and neutral, and the rest mean what they mean on a `Button`.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/iconbutton/#configurator-iconbutton), [Variants](https://romanhaendler.github.io/umriss-ui/core/iconbutton/#variants), [Sizes](https://romanhaendler.github.io/umriss-ui/core/iconbutton/#sizes) and 3 more |
| `size` | [`ButtonSize`](https://romanhaendler.github.io/umriss-ui/core/iconbutton/#type-ButtonSize)<br>`"sm" \| "md"` | the size of a `ControlSizeProvider` around it, else `"md"` | `sm` for buttons in header bars and table rows, `md` otherwise. From `ButtonProps`.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/iconbutton/#configurator-iconbutton), [Sizes](https://romanhaendler.github.io/umriss-ui/core/iconbutton/#sizes), [Build a toolbar](https://romanhaendler.github.io/umriss-ui/core/iconbutton/#a-toolbar) and 57 more |
| `loading` | `boolean` | `false` | Shows a loading indicator and locks the button. From `ButtonProps`.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/iconbutton/#configurator-iconbutton), [Loading and disabled](https://romanhaendler.github.io/umriss-ui/core/iconbutton/#loading-and-disabled), [Configurator (Button)](https://romanhaendler.github.io/umriss-ui/core/button/#configurator-button) and 4 more |
| `ref` | `Ref<HTMLButtonElement>` | — | Receives the component's root element. From `RefProp`.<br>Shown in: [A row that holds still (Sizes)](https://romanhaendler.github.io/umriss-ui/core/sizes/#a-row-that-holds-still), [An anchored surface (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#anchored-surface), [Take the width of the trigger (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#width-from-anchor) and 2 more |

#### Accessibility

| Prop | Type | Default | Description |
|---|---|---|---|
| `aria-label` *required* | `string` | — | The name: what a screen reader announces and the tooltip shows. Required - an icon alone names nothing.<br>Shown in: [Configurator](https://romanhaendler.github.io/umriss-ui/core/iconbutton/#configurator-iconbutton), [Name the icon](https://romanhaendler.github.io/umriss-ui/core/iconbutton/#name-the-icon), [Variants](https://romanhaendler.github.io/umriss-ui/core/iconbutton/#variants) and 5 more |

Also takes every attribute of `<button>`.

### Types on this page

#### `ButtonVariant`

How loud a button is: `primary` for the one main action, `secondary` by
default, `ghost` quiet in the accent, `plain` quiet and neutral, `danger`
for a destructive action.

```ts
type ButtonVariant = "primary" | "secondary" | "ghost" | "plain" | "danger";
```

#### `ButtonSize`

A button's two heights - the controls' (`ControlSize`).

```ts
type ButtonSize = ControlSize;
```

Resolves to `"sm" | "md"`.

#### `ControlSize`

The two heights of a control: `sm` for a toolbar and dense forms, `md`
otherwise.

```ts
type ControlSize = "sm" | "md";
```

## Known limits

- The tooltip always shows the name; a hint that says more than the name is a `Tooltip` around a `Button`.
- A touch screen shows no tooltip: choose icons whose meaning is common, or a `Button` with a label.

What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/).
