DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

IconButton

A square button that renders a single icon, with optional external label and notification badge.

Playground

Installation

pnpm add @tessinaui/ui

Usage

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:

PropTypeDefaultDescription
iconReactNode—The icon rendered inside the button
aria-labelstring—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
labelstring—Visible label text (used when labelPosition !== "none")
showBadgebooleanfalseShow a notification badge on the top-trailing corner
badgeCountstring | number—Badge content. Numbers get count semantics and cap at 99+; strings render verbatim. Omit it for a bare dot
loadingbooleanfalseOverlays a spinner, sets aria-busy + aria-disabled, swallows activation. The button keeps focus and its size; it is not natively disabled
renderReact.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.

KeyResult
TabMoves focus to the button
Enter / SpaceActivates it
  • type="button" by default, so it never submits a surrounding form.
  • loading sets aria-busy and 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". (An aria-label on 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 label and labelPosition to show one.
  • Pair it with a Tooltip. The aria-label reaches 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 carries aria-pressed, which this does not.
HoverCardLabel

On this page

PlaygroundInstallationUsageExamplesDefaultVariantsIntentsSizesBadgeStatesAPI ReferencePropsVariantsVariant OptionsIntent OptionsSize OptionsRoundedOn a coloured surfaceAccessibilityTap targets at a layout edgeWhen to use which