Skip to content

Icon Button

A button that displays only an icon, used for compact actions.

  • Icon Action
  • Tool Button

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

VariantPurposeVisual Treatment
FilledHigh-emphasis action needing attentionSolid background fill, contrasting icon color
TonalMedium-emphasis, softer than filledTinted background (e.g., primary at 10% opacity), matching icon color
OutlinedVisible boundary, lower emphasis than filled1px border, transparent background, icon inherits text color
GhostMinimal visual footprint, highest densityNo background, no border. Background appears on hover.
StandardDefault, icon with subtle interaction feedbackNo visible container at rest. Hover shows circular background.

Size Variants

SizeButton SizeIcon SizeTouch TargetUse Case
Extra Small24px14px24px minInline within text, badge decorations
Small32px16px32px (needs padding to reach 44px)Dense toolbars, table actions
Medium40px20px40px (needs padding to reach 44px)Standard UI, navigation
Large48px24px48pxMobile-first, primary icon actions

Shape Variants

ShapeRadiusUse Case
Circle50%Default — most recognizable icon button shape
Rounded Square--radius-md (8px)When icon buttons sit alongside rounded-rectangle elements
Square0pxDense toolbars, grid layouts

Properties

Icon Button Properties

PropertyTypeDefaultDescription
iconReactNode—Required. The icon element to display
aria-labelstring—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
disabledbooleanfalseDisables interaction
loadingbooleanfalseShows spinner, disables interaction
toggledboolean—For toggle icon buttons (favorite, bookmark). Controls filled/unfilled state.
tooltipstring—Tooltip text shown on hover. Falls back to aria-label if not provided.
asElementType'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 CategoryToken ExampleIcon Button Usage
Color – Icon--color-text-primaryDefault 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-600Filled variant background
Color – Filled Icon--color-on-primaryIcon color on filled variant
Color – Tonal BG--color-primary-100Tonal variant background
Color – Tonal Icon--color-primary-700Icon color on tonal variant
Color – Border--color-border-defaultOutlined variant border
Color – Disabled--color-text-disabledDisabled icon color
Sizing40px (md)Button width and height
Border – Radius50% / --radius-md / 0Per 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

StateVisual TreatmentNotes
DefaultIcon at standard opacity, no visible container (standard/ghost)The resting state
HoverCircular or rounded-square background fades in behind the iconBackground color: --color-action-hover
Active/PressedDarker background, optional scale-down (0.95)Provides tactile press feedback
FocusedFocus ring around the button boundaryUse outline-offset for circular buttons to prevent clipping
DisabledMuted icon color, no hover effect, cursor: not-allowedReduce opacity to ~0.38 or use --color-text-disabled
LoadingSpinner replaces icon, same dimensionsSpinner should match the icon's size and color
Toggled OnFilled icon, optional tonal backgroundFor favorites, bookmarks, pins — icon fills in when toggled
Toggled OffOutlined/stroked iconDefault unfilled state for toggle buttons

Toggle Icon Button States (e.g., Favorite)

StateIcon StyleBackgroundARIA
Off (unfavorited)Outlined heartNone (standard)aria-pressed="false"
Off + HoverOutlined heartSubtle hover backgroundaria-pressed="false"
On (favorited)Filled red heartOptional tonal pinkaria-pressed="true"
On + HoverFilled heartDarker tonal pinkaria-pressed="true"
AnimatingScale-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-label or aria-labelledby providing 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-pressed with a static label "Favorite"
  • Don't use title as 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% — use outline-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

html
<!-- 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>
tsx
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).

ActionsIcon ActionTool Button