Content
- The
aria-labelis a verb phrase in sentence case: "Copy profile link", "More actions".
An action shown only as an icon (close, more, copy). Always round; always named.
import { … } from "@brdgapp/ui/components/icon-button"IconButton is the button that shows only an icon: close, more actions, back, copy, sync. It is always round and always has a name for screen readers (aria-label is required). The usual mistakes are hand-making a small round <button>, and naming it after the picture ("X") instead of the action ("Close").
The look comes from props
A component's appearance comes only from its own options (variant, size, color). className on a package component is for layout (mt-4, shrink-0, w-full), never for font size, color, padding, radius or hover. If the look needs a className, the component or the chosen option is wrong: fix that instead.
One icon set, one icon per meaning
Every icon is Lucide at its own 2px line. Each meaning has one icon, listed on Iconography; a meaning that isn't listed gets a Lucide icon nobody else uses, added to the list in the same change. No other icon set, no emoji in the interface, and no hand-drawn company logos: BrandMark draws them.
Every control has a name
Every control says what it does to a screen reader. An icon-only button carries an aria-label naming the action ("Close", not "X"); a group of filters or a table has a name; a tooltip is never the only name.
Container. A circle: 28, 32 or 36px.
Icon. 16px, hidden from screen readers. The aria-label is the name.
Spinner. Replaces the icon while loading.
Each has an aria-label naming the action.
28, 32 and 36px.
Bridge's sync control is always a refresh IconButton.
| Rest | Hover | Focus | Disabled | Loading | |
|---|---|---|---|---|---|
| Default | |||||
| Danger |
| State | What happens |
|---|---|
| Rest | Muted icon, no fill. |
| Hover | A light fill and darker icon. danger turns red. |
| Focus visible | 2px ring with a 2px offset. |
| Disabled | 30% opacity. |
| Loading | Spinner, disabled, aria-busy. Put the progress in the label: "Syncing accounts…". |
type="button".loading shows the spinner, then a check when done or an alert icon with variant="danger" on failure. The state is in the aria-label, not visible text.Do
Name the action:aria-label="Close".Don't
Name the picture:aria-label="X icon".Do
Pass a bare icon; the button sizes it.Don't
SetclassName="h-5 w-5" on the icon.aria-label is a verb phrase in sentence case: "Copy profile link", "More actions".aria-label is required by the type, so a nameless icon button can't compile.sm (28px) is the smallest.| Keys | Action |
|---|---|
| Tab | Moves focus to the button. |
| EnterSpace | Presses it. |
IconButton
The icon-only button, built on Base UI Button. Extends Base UI Button (native <button> attributes).
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | — | Required. What the button does. |
variant | "default" | "danger" | "default" | danger for removing or deleting. |
size | "sm" | "md" | "lg" | "md" | 28, 32 or 36px. |
loading | boolean | false | Spinner, disabled, aria-busy. |
children | ReactNode | — | One bare Lucide icon. |
className | string | — | Layout only. Merged last. |
iconButtonVariants
The class recipe behind IconButton.
| Prop | Type | Default | Description |
|---|
What it's built from and where it's used, read from the code. When you change it, check these too.
components.md → the button-system family (IconButton, "Sync now" convention).src/components/ui/icon-button.tsx.