Icon Button

A Button with no words: a square box, and a name you cannot forget to give it.

import { IconButton } from "@dede-style/ui/button", then <IconButton glyph={iconClose} label="Close" />. It shares every variant, size, state and focus ring with Button — not by convention but by construction, because there is only one implementation.

It reimplements nothing. The fill, the hover, the press, the focus ring, the radius and the size ramp all live in Button and reach this through one shared stylesheet. Change the hover colour once and both change — which is the whole point of a wrapper, and the reason this is not a second copy of the same component.

The only thing it adds is a square box and a rule about the name.

Why it is its own component

On a normal button the visible words are the accessible name. Here there are none, so a name has to be typed in — and only a separate component can make that a build error rather than a review comment. A shared Button with an isIconOnly flag cannot: whenever children are present they satisfy the name, so nothing in the types can tell a deliberate icon-only button from a label somebody forgot.

So Button has no isIconOnly prop. Shipping both a flag and a component would be two ways to do one thing, and two ways to do one thing is how they drift apart.

It does not compose <Button> internally either, which looks like the obvious move and is not: Button requires children, so wrapping it would mean adding the very escape hatch this split exists to avoid. Two small components sharing one stylesheet is a cleaner boundary than one component with a mode.

Size and shape

The same four steps as Button, and the one shape name that means something different here.

6 · 24px
8 · 32px
10 · 40px
12 · 48px
rounded
pill
square

24px is the size this component exists at the bottom of the ramp for. It is exactly the minimum target size a pointer is entitled to, so it passes with nothing to spare — and it is too short to hold a form input, which is why the labelled control set really starts at 32. Reach for it in a dense toolbar, not by default.

shape="pill" renders a circle — the box is already square, so the full radius closes it without needing a second name. That is the one place the shared vocabulary reads differently here than on a labelled button, and it costs nothing.

Accessibility

The longest band on the shortest page, because the name is the entire component.

Those three look like glyphs and announce as “Open menu”, “Close” and “Search”. Nothing on screen shows the difference, which is exactly why this is a required prop rather than a documented convention.

The name goes on the button, never on the glyph. label becomes the button’s aria-label and the icon inside stays hidden. That ordering matters: the name then survives the icon being swapped, removed, or joined later by visible text. Put it on the icon instead and it walks away the moment the artwork changes.

Name the action, not the picture. A magnifier is “Search”, not “magnifying glass”. This is the other half of how the glyphs themselves are named: a shape-named glyph like iconBell declines to claim a role precisely so the control can name one — the same bell is notifications here and reminders somewhere else.

A tooltip is not an accessible name. It appears on hover, so it does not exist for anyone using a keyboard or a screen reader alone. When Tooltip lands it will sit alongside label, not in place of it.