IconButton
A square, icon-only button. Same visual language as Button — six variants, three sizes, the same radius scale — but optimised for label-less actions in toolbars and action rails. The required aria-label prop is enforced at the TypeScript level so missing accessible names get caught at build time.
When to use IconButton vs Button leftIcon
Both IconButton and <Button leftIcon> render an icon inside a clickable surface — but they read very differently and serve different jobs.
- Reach for IconButton when the icon alone is unambiguous (cards, tables, toolbars, media controls, chat composer rails) and you want a tight square hit target. A row of IconButtons reads as a control surface.
- Reach for <Button leftIcon> when the action benefits from a verbal label and you want a forgiving rectangular target — primary CTAs, form submits, and stand-alone actions on a page.
- The aria-label requirement is the deciding accessibility tell. If the icon has no text companion,
IconButton's required prop catches missing labels at the type level. With<Button leftIcon>and no text child, the missing-label bug is silent.
Usage
import { IconButton } from '@bwo-ui/react';
<IconButton aria-label="Search">
<SearchIcon />
</IconButton>Variants
IconButton mirrors every variant from Button: primary, green, yellow, ghost, outline, and solid. Pick the colour weighting that matches the action's emphasis — primary for default actions, ghost for ambient toolbars, green for confirm, yellow for callouts, outline for secondary, solid for the loud hero icon.
<IconButton variant="primary" aria-label="Next"><Arrow /></IconButton>
<IconButton variant="green" aria-label="Confirm"><Check /></IconButton>
<IconButton variant="yellow" aria-label="Highlight"><Star /></IconButton>
<IconButton variant="ghost" aria-label="Menu"><Menu /></IconButton>
<IconButton variant="outline" aria-label="Edit"><Pencil /></IconButton>
<IconButton variant="solid" aria-label="Action"><Bolt /></IconButton>Note on solid: Button's solid variant gets its weight from extra padding and uppercase typography, neither of which applies to a square icon-only button. The IconButton equivalent keeps the primary colour and adds an elevation shadow, so it reads as the louder cousin of primary.
Sizes
Three sizes — sm (32 px), md (40 px, default), lg (48 px). The icon itself does not auto-scale; pass a larger inline SVG when you bump up the size if you need the visual to grow with the hit target.
<IconButton size="sm" aria-label="Next"><Arrow /></IconButton>
<IconButton aria-label="Next"><Arrow /></IconButton>
<IconButton size="lg" aria-label="Next"><Arrow /></IconButton>Corner radius
The radius prop accepts the shared Radius scale (none / sm / md / lg / pill). Omit it to inherit whatever --bwo-radius-current evaluates to in context.
<IconButton radius="none" aria-label="Add"><Plus /></IconButton>
<IconButton radius="sm" aria-label="Add"><Plus /></IconButton>
<IconButton radius="md" aria-label="Add"><Plus /></IconButton>
<IconButton radius="lg" aria-label="Add"><Plus /></IconButton>
<IconButton radius="pill" aria-label="Add"><Plus /></IconButton>Toolbar pattern
IconButton was built for tight horizontal action rails. Wrap them in a role="toolbar" container with a single aria-label describing the group, then let each button carry its own action-level label.
<div role="toolbar" aria-label="Item actions" style={{ display: 'inline-flex', gap: 6 }}>
<IconButton variant="ghost" size="sm" aria-label="Like"><Heart /></IconButton>
<IconButton variant="ghost" size="sm" aria-label="Share"><Share /></IconButton>
<IconButton variant="ghost" size="sm" aria-label="Delete"><Trash /></IconButton>
</div>Disabled
IconButton forwards every native button attribute, including disabled. For async actions, prefer disabling alongside a separate loading indicator — IconButton has no built-in loading prop because there is no label slot to swap with a spinner.
<IconButton aria-label="Delete" disabled>
<Trash />
</IconButton>Accessibility
- aria-label is required. The TypeScript signature enforces it — your build will fail before missing labels can ship.
- Keep labels verb-first and action-specific ("Delete row", not "Trash icon").
- Mark inline SVG with
aria-hiddenso screen readers do not announce the glyph alongside the button's label. - Native focus, hover, and disabled behaviour are preserved — IconButton renders a real
buttonelement.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'primary' | 'green' | 'yellow' | 'ghost' | 'outline' | 'solid' | 'primary' | Visual style. Mirrors the Button variants. |
size | 'sm' | 'md' | 'lg' | 'md' | 32 / 40 / 48 px square. |
radius | 'none' | 'sm' | 'md' | 'lg' | 'pill' | — | Corner radius preset. Omit to inherit `--bwo-radius-current` (defaults to 6 px globally). |
aria-label | string | — | Required. Describes the action for screen readers — icon-only buttons have no visible label. |
…rest | ButtonHTMLAttributes<HTMLButtonElement> | — | All native button attributes are forwarded — `onClick`, `disabled`, `type`, `form`, `name`, etc. |