opsinjs
ComponentsActions and forms

IconButton

A button whose only visible content is an icon, so it always requires an accessible name you supply. Prefer a labelled button wherever there is room for a word.

Preview

Theme
Density
Text
Status
icon-button · base base · style base-lyraOpen under the product theme

Installation

pnpm dlx shadcn@latest add @opsinjs/icon-button

The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.

Usage

import { IconButton } from "@/components/ui/icon-button"
import { Search } from "lucide-react"

<IconButton
  icon={<Search />}
  label="Search readings"
  onClick={openSearch}
/>

When to use it

Use it when

  • A dense toolbar or card header where a universally understood glyph, such as a close cross, stands in for a word that would not fit.
  • A repeated action in a list or table row, where a labelled button on every row would crowd the content out.
  • A secondary control beside a labelled primary, where the reader has already taken in the row's context.

Do not use it when

  • There is room for a word. A short verb beats a glyph almost everywhere.Use button instead.
  • The control turns something on or off, where a glyph reads as a state rather than an action.Use switch instead.
  • You were about to explain the icon through a hover tooltip. There is no hover on a phone.Use tooltip instead.

Anatomy

  1. IconButtonThe root: a wrapper span carrying data-slot="icon-button" and no styling. className goes to the composed Button, not here.
  2. ButtonThe pressable element, a composed Button. Square at the 44pt floor, with fill, ink, boundary and focus ring inherited.Controlled by variant
  3. IconButton.IconThe glyph and the only visible content. Always aria-hidden, inside Button's icon slot, with any child svg at 1em.Controlled by icon
  4. IconButton.NameThe accessible name, a visually hidden text node carrying label. The same string is also aria-label on the button.Controlled by label

Examples

A toolbar of icon buttons

A header row where each glyph is one the reader already knows. Each control still carries a distinct label, because a toolbar is where an unnamed icon button disappears.

Theme
Density
Text
Status
icon-button-toolbar · base base · style base-lyraOpen under the product theme

Icon buttons in a list row

Repeated row actions, where a labelled Edit and Remove on every row would crowd the reading out. Each label carries its row's context, so a voice-control user reaches one control.

Theme
Density
Text
Status
icon-button-list-row · base base · style base-lyraOpen under the product theme

Content guidelines

The label is the whole message for a reader who does not see the glyph. Name the action and its object, in sentence case, without terminal punctuation. Choose a glyph the audience already knows, and let the glyph and the name agree: a magnifier means search, a cross means close.

Do

label="Close the reading details" with a cross glyph. A verb and its object reads well aloud.

Don’t

label="button", or no label at all. A screen reader announces "button" and nothing else.

Accessibility

Checked by pnpm run check:a11y on every commit. Every colour is a role token, no type size is in px, and no banned word appears anywhere.

Audited against WCAG 2.2 AA, source and rendered passes, author-run and not independent. Clinical review is pending. The audit found no critical, serious or moderate defect in the source.

  • A real button element through Button supplies Return, Space, the role and the tab stop.
  • The name is aria-label plus a visually hidden text node; a missing or whitespace-only label raises a development warning.
  • The glyph is aria-hidden, never announced as a second, pictorial name.
  • The target is a 44pt square in rem, with horizontal padding removed and separation left to the caller's --opsin-target-separation.

Icon-only by design. With no visible text, aria-label plus the hidden text node is the correct and complete name route rather than a persistent visible label. The docblock warns in development on a missing label.

Not measured. Contrast on every variant, the focus ring, and forced colours, all inherited from Button.

KeyActionNotes
TabMoves focus to the icon buttonOne tab stop for the whole control. The glyph and name are parts, not stops.
Shift+TabMoves focus back out, to whatever precedes itReverse order matches the visual order. The component never moves focus of its own accord.
EnterActivates the buttonPlatform behaviour on a real button element, inherited from Button. Nothing here intercepts it.
SpaceActivates the buttonConsumed by the focused button, so the page does not scroll.
PairThemeAPCA LcWCAG 2.2Floor
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below floor

These are the measured token pairs this component draws colour from, not a measurement of the component itself.

API reference

Prop

Type

Generated from IconButtonProps in registry/bases/base/icon-button.tsx.

icon and label are both required. variant defaults to secondary over quiet, since a bordered control is easier to recognise without a word inside it. There is no busy, busyLabel, fullWidth, iconPosition or type. className must never resolve a --opsin-status-* or --opsin-category-* role; an IconButton carries no clinical axis.

  • Button is the labelled control this one defers to.
  • Switch turns something on or off; an icon button acts and does not hold a setting.
  • Tooltip supplements a name and is never the name itself.

On this page