Loading…
Loading…
A button that displays only an icon, used for compact actions.
The Icon Button is a compact button that displays only an icon, without visible text. It's the workhorse of dense interfaces — toolbars, card actions, table row operations, navigation headers, and anywhere space is limited but actions are needed.
Icon buttons trade label clarity for spatial efficiency. This trade-off is only justified when the icon's meaning is universally understood (close ×, search 🔍, menu ☰, share, favorite heart, delete trash) or when the context makes the action obvious (a pencil icon on an editable card). For any icon whose meaning might be ambiguous, either use a standard Button with text or add a Tooltip to the icon button.
When to use an Icon Button:
When NOT to use an Icon Button:
<span> with the iconAlways pair icon buttons with an aria-label. Use the Button Generator to design the button's padding, background, and border-radius. Verify icon-to-background contrast with the Contrast Checker.
| Variant | Purpose | Visual Treatment |
|---|---|---|
| Filled | High-emphasis action needing attention | Solid background fill, contrasting icon color |
| Tonal | Medium-emphasis, softer than filled | Tinted background (e.g., primary at 10% opacity), matching icon color |
| Outlined | Visible boundary, lower emphasis than filled | 1px border, transparent background, icon inherits text color |
| Ghost | Minimal visual footprint, highest density | No background, no border. Background appears on hover. |
| Standard | Default, icon with subtle interaction feedback | No visible container at rest. Hover shows circular background. |
| Size | Button Size | Icon Size | Touch Target | Use Case |
|---|---|---|---|---|
| Extra Small | 24px | 14px | 24px min | Inline within text, badge decorations |
| Small | 32px | 16px | 32px (needs padding to reach 44px) | Dense toolbars, table actions |
| Medium | 40px | 20px | 40px (needs padding to reach 44px) | Standard UI, navigation |
| Large | 48px | 24px | 48px | Mobile-first, primary icon actions |
| Shape | Radius | Use Case |
|---|---|---|
| Circle | 50% | Default — most recognizable icon button shape |
| Rounded Square | --radius-md (8px) | When icon buttons sit alongside rounded-rectangle elements |
| Square | 0px | Dense toolbars, grid layouts |
| Property | Type | Default | Description |
|---|---|---|---|
icon | ReactNode | — | Required. The icon element to display |
aria-label | string | — | Required. Accessible name describing the action (e.g., "Close dialog", "Delete item") |
variant | 'filled' | 'tonal' | 'outlined' | 'ghost' | 'standard' | 'standard' | Visual style |
size | 'xs' | 'sm' | 'md' | 'lg' | 'md' | Button dimensions and icon size |
shape | 'circle' | 'rounded' | 'square' | 'circle' | Border radius style |
color | 'default' | 'primary' | 'error' | 'success' | 'default' | Color theme |
disabled | boolean | false | Disables interaction |
loading | boolean | false | Shows spinner, disables interaction |
toggled | boolean | — | For toggle icon buttons (favorite, bookmark). Controls filled/unfilled state. |
tooltip | string | — | Tooltip text shown on hover. Falls back to aria-label if not provided. |
as | ElementType | 'button' | Polymorphic element override |
onClick | () => void | — | Click handler |
Important: aria-label is not optional. Without it, screen readers announce the button as "button" with no context. The label should describe the action, not the icon: "Delete message" not "Trash can". See the Button component for additional shared button props.
| Token Category | Token Example | Icon Button Usage |
|---|---|---|
| Color – Icon | --color-text-primary | Default icon color (standard/ghost variants) |
| Color – Hover BG | --color-action-hover (e.g., rgba(0,0,0,0.04)) | Background on hover for ghost/standard variants |
| Color – Active BG | --color-action-active (e.g., rgba(0,0,0,0.08)) | Background on press |
| Color – Filled BG | --color-primary-600 | Filled variant background |
| Color – Filled Icon | --color-on-primary | Icon color on filled variant |
| Color – Tonal BG | --color-primary-100 | Tonal variant background |
| Color – Tonal Icon | --color-primary-700 | Icon color on tonal variant |
| Color – Border | --color-border-default | Outlined variant border |
| Color – Disabled | --color-text-disabled | Disabled icon color |
| Sizing | 40px (md) | Button width and height |
| Border – Radius | 50% / --radius-md / 0 | Per shape variant |
| Transition | --duration-fast (150ms) | Hover/active background transitions |
The ghost/standard variant's hover background is critical: it must be subtle enough not to compete with the icon but visible enough to signal interactivity. An alpha-based color (rgba(0,0,0,0.04)) works across light/dark themes better than a hardcoded gray.
| State | Visual Treatment | Notes |
|---|---|---|
| Default | Icon at standard opacity, no visible container (standard/ghost) | The resting state |
| Hover | Circular or rounded-square background fades in behind the icon | Background color: --color-action-hover |
| Active/Pressed | Darker background, optional scale-down (0.95) | Provides tactile press feedback |
| Focused | Focus ring around the button boundary | Use outline-offset for circular buttons to prevent clipping |
| Disabled | Muted icon color, no hover effect, cursor: not-allowed | Reduce opacity to ~0.38 or use --color-text-disabled |
| Loading | Spinner replaces icon, same dimensions | Spinner should match the icon's size and color |
| Toggled On | Filled icon, optional tonal background | For favorites, bookmarks, pins — icon fills in when toggled |
| Toggled Off | Outlined/stroked icon | Default unfilled state for toggle buttons |
| State | Icon Style | Background | ARIA |
|---|---|---|---|
| Off (unfavorited) | Outlined heart | None (standard) | aria-pressed="false" |
| Off + Hover | Outlined heart | Subtle hover background | aria-pressed="false" |
| On (favorited) | Filled red heart | Optional tonal pink | aria-pressed="true" |
| On + Hover | Filled heart | Darker tonal pink | aria-pressed="true" |
| Animating | Scale-up + color transition | — | — |
Icon buttons are among the most frequently inaccessible components in the wild. The absence of visible text makes proper ARIA labeling absolutely critical.
Accessible Name (WCAG 4.1.2 – Name, Role, Value):
aria-label or aria-labelledby providing a clear action descriptionaria-pressed with a static label "Favorite"title as the only accessible name — its behavior is inconsistent across screen readersRole and State (WCAG 4.1.2):
<button> element is sufficient — no additional role neededaria-pressed="true|false" to communicate the toggled state<div> or <span> with click handlers — always use <button> or <a> (if navigation)Target Size (WCAG 2.5.8 – Target Size Minimum, 2.5.5 – Target Size Enhanced):
Color Contrast (WCAG 1.4.3, 1.4.11):
Keyboard (WCAG 2.1.1):
role="toolbar"), arrow keys navigate between buttons, Tab enters/exits the toolbarborder-radius: 50% — use outline-offsetTooltips (WCAG 1.3.1, Content on Hover 1.4.13):
Do:
aria-label — this is the #1 rule for icon buttonsDon't:
aria-label — an icon without a name is invisible to screen readers<!-- Standard icon button (ghost) -->
<button class="icon-btn" aria-label="Search">
<svg class="icon-btn-svg" aria-hidden="true" viewBox="0 0 24 24">
<circle cx="11" cy="11" r="8" fill="none" stroke="currentColor" stroke-width="2"/>
<path d="M21 21l-4.35-4.35" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
<!-- Filled icon button -->
<button class="icon-btn icon-btn--filled" aria-label="Add to favorites">
<svg class="icon-btn-svg" aria-hidden="true" viewBox="0 0 24 24">
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="currentColor"/>
</svg>
</button>
<!-- Toggle icon button -->
<button class="icon-btn" aria-label="Bookmark" aria-pressed="false">
<svg class="icon-btn-svg" aria-hidden="true" viewBox="0 0 24 24">
<path d="M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2z" fill="none" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<!-- Icon button in toolbar -->
<div role="toolbar" aria-label="Text formatting">
<button class="icon-btn" aria-label="Bold" aria-pressed="false">
<svg class="icon-btn-svg" aria-hidden="true" viewBox="0 0 24 24"><path d="M6 4h8a4 4 0 014 4 4 4 0 01-4 4H6z M6 12h9a4 4 0 014 4 4 4 0 01-4 4H6z" fill="none" stroke="currentColor" stroke-width="2"/></svg>
</button>
<button class="icon-btn" aria-label="Italic" aria-pressed="false">
<svg class="icon-btn-svg" aria-hidden="true" viewBox="0 0 24 24"><line x1="19" y1="4" x2="10" y2="4" stroke="currentColor" stroke-width="2"/><line x1="14" y1="20" x2="5" y2="20" stroke="currentColor" stroke-width="2"/><line x1="15" y1="4" x2="9" y2="20" stroke="currentColor" stroke-width="2"/></svg>
</button>
</div>
<style>
.icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border: none;
border-radius: 50%;
background: transparent;
color: var(--color-text-primary);
cursor: pointer;
padding: 0;
transition: background-color 0.15s ease;
}
.icon-btn:hover {
background: rgba(0, 0, 0, 0.04);
}
.icon-btn:active {
background: rgba(0, 0, 0, 0.08);
}
.icon-btn:focus-visible {
outline: 2px solid var(--color-primary-500);
outline-offset: 2px;
}
.icon-btn[aria-pressed="true"] {
color: var(--color-primary-600);
background: var(--color-primary-50);
}
.icon-btn--filled {
background: var(--color-primary-600);
color: var(--color-on-primary);
}
.icon-btn--filled:hover {
background: var(--color-primary-700);
}
.icon-btn-svg {
width: 20px;
height: 20px;
}
</style>import React from 'react';
import styles from './IconButton.module.css';
import clsx from 'clsx';
interface IconButtonProps {
icon: React.ReactNode;
'aria-label': string;
variant?: 'filled' | 'tonal' | 'outlined' | 'ghost' | 'standard';
size?: 'xs' | 'sm' | 'md' | 'lg';
shape?: 'circle' | 'rounded' | 'square';
color?: 'default' | 'primary' | 'error' | 'success';
disabled?: boolean;
loading?: boolean;
toggled?: boolean;
tooltip?: string;
as?: React.ElementType;
onClick?: () => void;
}
export function IconButton({
icon,
'aria-label': ariaLabel,
variant = 'standard',
size = 'md',
shape = 'circle',
color = 'default',
disabled = false,
loading = false,
toggled,
tooltip,
as: Component = 'button',
onClick,
}: IconButtonProps) {
const isToggle = toggled !== undefined;
return (
<Component
className={clsx(
styles.root,
styles[variant],
styles[size],
styles[shape],
styles[color],
{ [styles.toggled]: toggled, [styles.loading]: loading }
)}
aria-label={ariaLabel}
aria-pressed={isToggle ? toggled : undefined}
disabled={disabled || loading}
onClick={onClick}
title={tooltip || ariaLabel}
type={Component === 'button' ? 'button' : undefined}
>
{loading ? (
<span className={styles.spinner} aria-hidden="true" />
) : (
<span className={styles.icon} aria-hidden="true">{icon}</span>
)}
</Component>
);
}Material Design (MUI) provides <IconButton> with size (small, medium, large), color (default, inherit, primary, secondary, error, info, success, warning), and edge (start, end, false) for alignment in list items and toolbars. MUI's IconButton renders a <button> with a circular ripple effect. The edge prop adjusts negative margin to align the icon with content edges. MUI v5 added disableRipple for accessibility concerns with the ripple animation. Icon size is controlled by wrapping the icon in MUI's <SvgIcon> with fontSize prop.
Ant Design does not have a dedicated IconButton. Instead, Ant's <Button> with type="text", shape="circle", and icon={<IconComponent />} creates the equivalent. Ant's icon system (@ant-design/icons) provides consistent 14px/16px/20px icon sizes. For toolbar-style icon buttons, Ant recommends <Space.Compact> wrapping <Button shape="circle"> elements.
Chakra UI provides <IconButton> with icon, aria-label (required), variant (solid, outline, ghost, link), size, colorScheme, and isRound props. Chakra enforces aria-label as a required prop at the TypeScript level — builds fail without it. The isRound prop toggles between circular and rounded-square shapes.
Bootstrap uses .btn with no text content, just an icon element inside. There is no dedicated IconButton class. Teams typically add custom CSS for consistent square sizing: .btn-icon { width: 40px; height: 40px; padding: 0; display: inline-flex; align-items: center; justify-content: center; }. Bootstrap Icons (bootstrap-icons) provides the icon library.
Apple Human Interface Guidelines treats icon-only buttons as standard buttons with SF Symbols icons. In SwiftUI, Button(action: {}) { Image(systemName: "heart") }.buttonStyle(.plain) creates an icon button. Apple requires all icon buttons to have accessibility labels: .accessibilityLabel("Add to favorites"). Apple recommends a minimum 44×44pt touch target for all interactive elements — enforced by Accessibility Inspector. See the Button component for Apple's broader button guidance.
Tailwind CSS icon buttons are composed with utilities: inline-flex items-center justify-center w-10 h-10 rounded-full hover:bg-gray-100 transition-colors. The group class enables hover-triggered tooltip display. Headless UI's <Button> and Radix's <ToggleButton> provide the behavioral primitives. Icon sizing typically uses Heroicons (24px) or Lucide (24px default, configurable).