Overview
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:
- In toolbars and action bars where multiple actions compete for space
- For universally recognized actions: close, back, menu, search, settings, favorite, share
- As card or list item actions (edit, delete, more options)
- In navigation headers (back arrow, hamburger menu)
- For media controls (play, pause, skip, volume)
When NOT to use an Icon Button:
- When the icon's meaning is ambiguous — use a text Button instead
- For the primary page CTA — use a labeled button for maximum clarity
- When there's enough space for a text label — text is always more accessible than icons
- For decorative icons that don't trigger actions — use a plain
<span>with the icon
Always 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.
Variants
Common Icon Button Variants
| 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 Variants
| 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 Variants
| 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 |
Properties
Icon Button Properties
| 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 Mappings
Design Token Mappings
| 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.
States
Icon Button States
| 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 |
Toggle Icon Button States (e.g., Favorite)
| 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 | — | — |
Accessibility
Accessibility
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):
- Every icon button MUST have
aria-labeloraria-labelledbyproviding a clear action description - The label should describe the action, not the icon: "Close dialog" not "X", "Delete message" not "Trash"
- For toggle buttons, the label should reflect the current or target state: "Add to favorites" (when unfavorited), or use
aria-pressedwith a static label "Favorite" - Don't use
titleas the only accessible name — its behavior is inconsistent across screen readers
Role and State (WCAG 4.1.2):
- Standard icon buttons: native
<button>element is sufficient — no additional role needed - Toggle icon buttons: add
aria-pressed="true|false"to communicate the toggled state - Don't use
<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):
- WCAG 2.2 Level AA requires 24×24 CSS pixels minimum (SC 2.5.8)
- Level AAA requires 44×44 CSS pixels (SC 2.5.5)
- Small icon buttons (32px or less) should have padding or margin to ensure the tap target reaches at least 44px on touch devices
- In dense toolbars, ensure at least 8px spacing between adjacent icon buttons
Color Contrast (WCAG 1.4.3, 1.4.11):
- Icon color must achieve 3:1 against the button background (WCAG 1.4.11 – Non-text Contrast) — icons are graphical objects
- On hover, the icon must still maintain contrast against the hover background
- For filled variant: icon-on-fill must achieve 3:1 minimum. Use the Contrast Checker.
Keyboard (WCAG 2.1.1):
- Must be focusable and activatable with Enter/Space
- In toolbars (
role="toolbar"), arrow keys navigate between buttons, Tab enters/exits the toolbar - Focus ring must be visible and not clipped by
border-radius: 50%— useoutline-offset
Tooltips (WCAG 1.3.1, Content on Hover 1.4.13):
- Tooltips on icon buttons should show the action label on hover and keyboard focus
- Tooltip must be dismissible (Escape), hoverable (mouse can enter tooltip), and persistent (doesn't disappear while hovering)
- See Tooltip component for full requirements
Usage Guidelines
Usage Guidelines
Do:
- Always provide
aria-label— this is the #1 rule for icon buttons - Add a Tooltip for any icon that's not universally recognized
- Use universally understood icons: ✕ (close), ← (back), ☰ (menu), 🔍 (search), ⚙ (settings), ❤ (favorite), 🗑 (delete)
- Maintain minimum 44px touch targets on mobile — pad small buttons with invisible touch areas
- Use consistent icon sizes within the same toolbar or context
- Pair with the Button component for primary actions that need text labels
Don't:
- Don't use icon buttons without
aria-label— an icon without a name is invisible to screen readers - Don't use ambiguous icons without tooltips — a pencil icon means "edit" in one context and "draw" in another
- Don't mix icon button sizes within the same toolbar
- Don't use icon buttons for destructive actions without confirmation — accidental clicks are more likely on small targets
- Don't use custom/novel icons for critical actions — stick to established iconography
- Don't put text inside an icon button — that's a regular button. Use Button.
Code Snippets
<!-- 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>
);
}Design Systems
Design System Implementations
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).