IconButton
A square button that renders a single icon, with optional external label and notification badge.
Playground
Installation
pnpm add @tessinaui/uiUsage
import { IconButton } from "@tessinaui/ui";
import { Bell } from "lucide-react";
<IconButton icon={<Bell />} aria-label="Notifications" />Examples
Default
A basic icon button with the default primary variant.
Variants
Four visual styles — primary, secondary, ghost, and outline.
Intents
Semantic colors for actions — error, warning, success, and info.
Sizes
Five square sizes from xs through xl.
Badge
Overlay a notification badge, optionally with a count. This is the library
Badge — showBadge alone renders a bare dot, and a
badgeCount renders a count pill that caps at 99+. Pass a string
(badgeCount="99+") to bypass the cap.
States
Default, disabled, and loading states.
API Reference
Props
The IconButton component extends native HTML button attributes (excluding children) and includes:
| Prop | Type | Default | Description |
|---|---|---|---|
icon | ReactNode | — | The icon rendered inside the button |
aria-label | string | — | Accessible name. Required — or pass aria-labelledby instead; the type accepts either |
variant | "primary" | "secondary" | "ghost" | "outline" | "primary" | The visual style variant |
intent | "none" | "error" | "warning" | "success" | "info" | "none" | The semantic intent color |
size | "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Square size on the family scale: 24 / 32 / 40 / 48 / 56 / 96 px. 2xs–sm carry a 44 px tap extension on phones |
rounded | "none" | "sm" | "md" | "lg" | "xl" | "full" | "full" | Corner radius; the badge follows it |
tone | "default" | "on-color" | inherited from <Surface> | High-contrast styling for a coloured or dark surface |
labelPosition | "none" | "top" | "bottom" | "start" | "end" | "none" | Position of the external text label |
label | string | — | Visible label text (used when labelPosition !== "none") |
showBadge | boolean | false | Show a notification badge on the top-trailing corner |
badgeCount | string | number | — | Badge content. Numbers get count semantics and cap at 99+; strings render verbatim. Omit it for a bare dot |
loading | boolean | false | Overlays a spinner, sets aria-busy + aria-disabled, swallows activation. The button keeps focus and its size; it is not natively disabled |
render | React.ReactElement | — | Render into another element — a <button>-like host, or an <a> which keeps link semantics (see Button → As a link). The element's own ref, onClick and onKeyDown are merged, not replaced |
tapEdge | "none" | "inline-start" | "inline-end" | "none" | Declares that the button sits flush against a layout edge, so its phone tap extension grows inward instead of across that edge. Logical, so it follows dir. Sizes md and up ignore it |
dir | "ltr" | "rtl" | "ltr" | Text direction for RTL layouts |
All standard HTML button attributes are also supported (onClick, disabled, type, etc.).
Variants
Variant Options
- primary: Filled button for main actions
- secondary: Gray button for alternative actions
- ghost: Transparent button with no background
- outline: Button with border, fills on hover and pressed states
Intent Options
Intent controls the semantic color of the button, independent of the variant style:
- none: Default color scheme
- error: Red color scheme for destructive or error actions
- warning: Yellow color scheme with dark text for warning actions
- success: Green color scheme for success or confirmation actions
- info: Blue color scheme for informational actions
Size Options
Squares on the button family's heights, so a md IconButton lines up with a md
Button:
- 2xs: 24 x 24 px
- xs: 32 x 32 px
- sm: 40 x 40 px
- md: 48 x 48 px (default)
- lg: 56 x 56 px
- xl: 96 x 96 px
2xs, xs and sm are under the 44px touch minimum, so on phones and tablets
they carry an invisible tap-area extension that reaches it without changing how
big the button looks. The extension is max-md only — on a pointer device it
would overlap neighbours in a tight toolbar.
Rounded
Every painted corner tracks the prop — including the notification badge, which used to stay a circle on a square button.
On a coloured surface
Accessibility
Follows the WAI-ARIA Button pattern.
The name is required. aria-label (or aria-labelledby) is not optional and
not a lint rule — it is in the type. The icon is aria-hidden, so a button
without one announces nothing at all.
| Key | Result |
|---|---|
| Tab | Moves focus to the button |
| Enter / Space | Activates it |
type="button"by default, so it never submits a surrounding form.loadingsetsaria-busyand keeps focus; the spinner is decorative and the square never changes size.- The badge is visual only — put the count in the button's name:
aria-label="Notifications, 3 unread". (Anaria-labelon the badge span would be ignored by assistive technology, so it does not carry one.) - Focus ring: 2px at 3:1, offset painted with the page background.
Tap targets at a layout edge
Sizes 2xs–sm are smaller than 44px, so they carry an invisible ::before
that grows the touch target to 44px. It is symmetric by default — 6px on each
side of an xs button.
That pseudo-element still counts toward its ancestors' scrollable overflow.
So a button mounted flush against a full-bleed edge — a CardRow action, a list
row's trailing control — pushes its extension past that edge and the page picks
up a few pixels of horizontal scroll: invisible, but real to a thumb on a phone.
CSS cannot ask "am I at the edge", so the placement has to say so:
<IconButton
size="xs"
icon={<ArrowDownToLine />}
aria-label="Download offline map"
tapEdge="inline-end"
/>The target stays 44×44 — an xs button biased to inline-end takes all 12px on
its inline-start side instead of 6px per side. Reach for it whenever a small
square sits against a container whose padding is zero; leave it alone otherwise,
because a centred extension is the better target when there is room on both
sides.
When to use which
- Only when the icon is unambiguous on its own — a close ✕, a search
magnifier, an overflow ⋯. If it needs explaining, use a Button with a label, or
pass
labelandlabelPositionto show one. - Pair it with a Tooltip. The
aria-labelreaches screen-reader users; a sighted user hovering an unfamiliar glyph has nothing without the tooltip. - For a two-state icon control (bold, mute, favourite), reach for
ToggleButton with
size="icon"— it carriesaria-pressed, which this does not.