IconButton
import { useState } from 'react'
import {
Icon,
IconButton,
Surface,
Text,
type IconButtonSize,
type IconButtonVariant,
type IconButtonWidth,
} from '@language-lit/material3-expressive'
const variants: readonly IconButtonVariant[] = ['standard', 'filled', 'tonal', 'outlined']
const sizes: readonly IconButtonSize[] = [
'extra-small',
'small',
'medium',
'large',
'extra-large',
]
const widths: readonly IconButtonWidth[] = ['narrow', 'uniform', 'wide']
export function IconButtonExample() {
const [favorite, setFavorite] = useState(false)
return (
<Surface
as="section"
aria-labelledby="icon-button-example-title"
color="surface-container-low"
shape="extra-large"
className="icon-button-example"
>
<Text as="h2" id="icon-button-example-title" variant="titleLarge" emphasis="emphasized">
Expressive icon buttons
</Text>
<Text as="p" variant="bodyMedium">
Native momentary and toggle actions with five sizes and shape-changing selection.
</Text>
<div className="icon-button-example__row" aria-label="Icon button variants">
{variants.map((variant) => (
<IconButton key={variant} aria-label={`${variant} add`} variant={variant}>
<Icon source="add" />
</IconButton>
))}
</div>
<div className="icon-button-example__row" aria-label="Icon button widths">
{widths.map((width) => (
<IconButton key={width} aria-label={`${width} search`} variant="outlined" width={width}>
<Icon source="search" />
</IconButton>
))}
</div>
<div className="icon-button-example__row" aria-label="Icon button widths (filled, toggle)">
{widths.map((width) => (
<IconButton
key={width}
aria-label={`${width} bookmark toggle`}
variant="filled"
width={width}
toggle
defaultSelected={width === 'wide'}
selectedIcon={<Icon source="bookmark" fill={1} />}
>
<Icon source="bookmark" />
</IconButton>
))}
</div>
<div className="icon-button-example__row" aria-label="Square-shaped icon button variants">
{variants.map((variant) => (
<IconButton
key={variant}
aria-label={`${variant} square edit`}
variant={variant}
shape="square"
>
<Icon source="edit" />
</IconButton>
))}
</div>
<div className="icon-button-example__row" aria-label="Icon button toggle variants">
<IconButton
aria-label="Standard toggle bold"
variant="standard"
toggle
defaultSelected
selectedIcon={<Icon source="format_bold" fill={1} />}
>
<Icon source="format_bold" />
</IconButton>
<IconButton
aria-label="Outlined toggle italic"
variant="outlined"
toggle
defaultSelected
selectedIcon={<Icon source="format_italic" fill={1} />}
>
<Icon source="format_italic" />
</IconButton>
</div>
<div className="icon-button-example__row" aria-label="Disabled icon button variants">
{variants
.filter((variant) => variant !== 'standard')
.map((variant) => (
<IconButton
key={variant}
aria-label={`Disabled ${variant}`}
variant={variant}
disabled
>
<Icon source="block" />
</IconButton>
))}
</div>
<IconButton
aria-label="Disabled selected favorite"
variant="filled"
toggle
defaultSelected
disabled
selectedIcon={<Icon source="favorite" fill={1} />}
>
<Icon source="favorite" />
</IconButton>
<div className="icon-button-example__sizes" aria-label="Expressive icon button sizes">
{sizes.map((size) => (
<IconButton
key={size}
aria-label={`${size} favorite`}
variant="tonal"
size={size}
shape={size === 'medium' ? 'square' : 'round'}
toggle
defaultSelected={size === 'large'}
selectedIcon={<Icon source="favorite" fill={1} />}
>
<Icon source="favorite" />
</IconButton>
))}
</div>
<IconButton
aria-label="Favorite"
variant="filled"
toggle
selected={favorite}
onSelectedChange={setFavorite}
selectedIcon={<Icon source="favorite" fill={1} />}
>
<Icon source="favorite" />
</IconButton>
<IconButton aria-label="Unavailable" disabled>
<Icon source="block" />
</IconButton>
</Surface>
)
}IconButton is a native icon-only action with the current Material 3
Expressive sizes, widths, shapes, and toggle behavior. It has no framework,
router, tooltip, or icon-package dependency.
import {
Icon,
IconButton,
} from '@language-lit/material3-expressive'
import '@language-lit/material3-expressive/styles.css'
<IconButton aria-label="Search">
<Icon source="search" />
</IconButton>Contract
- The root is always a native
<button>and its ref is anHTMLButtonElement. It defaults totype="button"; explicit submit/reset, form ownership, name, and value retain native behavior. variantisstandard(default),filled,tonal, oroutlined.sizeisextra-small,small(default),medium,large, orextra-large.widthisnarrow,uniform(default), orwide. These are sourced visual container widths, not page-layout or full-width modes.shapeisround(default) orsquare. Pressed and selected states morph to the current size tier's sourced shapes.childrenis the decorative default visual. PassIcon, SVG, or equivalent non-interactive artwork. A toggle may passselectedIconfor an alternate selected visual.
IconButton does not render a link or tooltip. Use a link for navigation and
associate any tooltip as a separate accessible description.
Toggle buttons
Toggle mode is explicit and uses native ARIA toggle-button semantics:
const [favorite, setFavorite] = useState(false)
<IconButton
aria-label="Favorite"
variant="filled"
toggle
selected={favorite}
onSelectedChange={setFavorite}
selectedIcon={<Icon source="favorite" fill={1} />}
>
<Icon source="favorite" />
</IconButton>Use selected with onSelectedChange for controlled state, or
defaultSelected for uncontrolled state. The button emits boolean
aria-pressed; momentary buttons omit it. onClick runs before internal state
behavior, and event.preventDefault() cancels the toggle. Enter, Space, pointer
activation, disabled behavior, and focus remain browser-owned.
Keep the accessible name stable, such as “Favorite.” The pressed state says
whether that action is active; changing the name between “Favorite” and
“Unfavorite” would make the meaning of aria-pressed ambiguous. An alternate
selected icon is recommended whenever the visual state would otherwise depend
only on color.
Expressive dimensions
| Size | Height | Icon | Narrow | Uniform | Wide | Outline |
|---|---|---|---|---|---|---|
extra-small | 32px | 20px | 28px | 32px | 40px | 1px |
small | 40px | 24px | 32px | 40px | 52px | 1px |
medium | 56px | 24px | 48px | 56px | 72px | 1px |
large | 96px | 32px | 64px | 96px | 128px | 2px |
extra-large | 136px | 40px | 104px | 136px | 184px | 3px |
The semantic root remains at least 48 by 48 CSS pixels even when the visual container is smaller or narrower. Large tiers are prominent actions, not density substitutes.
Shape and color
Round resting shapes use exact half-height radii. Square resting roles progress from medium to large and extra-large. Pressing morphs to small, medium, or large corners by tier. Toggle selection intentionally makes a round button more square and a square button fully round, following the current Expressive token pairs.
| Variant | Momentary/default | Toggle selected |
|---|---|---|
standard | transparent / on-surface-variant | transparent / primary |
filled | primary / on-primary | primary / on-primary |
tonal | secondary-container / on-secondary-container | secondary / on-secondary |
outlined | transparent / on-surface-variant with outline-variant | inverse-surface / inverse-on-surface, no outline |
Filled toggles use surface-container/on-surface-variant while unselected, then the selected filled pair. Disabled roles and opacities follow the pinned AndroidX defaults.
Accessibility
Every icon-only button requires a localized aria-label or
aria-labelledby; development builds warn when both are empty. The visual
container is aria-hidden, so even a mistakenly meaningful nested Icon
cannot create a duplicate name. Do not place text or another interactive
element inside the visual slot.
Keyboard focus uses a token-backed :focus-visible ring. Forced-colors mode
uses ButtonFace/ButtonText, Highlight/HighlightText for selected, GrayText for
disabled, and a visible border for every variant. Reduced motion removes shape,
color, and state-layer transitions while preserving immediate state changes.
Tokens
IconButton component variables follow literal groups:
--m3e-comp-icon-button-{size}-container-height--m3e-comp-icon-button-{size}-container-width-{narrow|uniform|wide}--m3e-comp-icon-button-{size}-icon-size--m3e-comp-icon-button-{size}-container-shape-{round|square}--m3e-comp-icon-button-{size}-pressed-container-shape--m3e-comp-icon-button-{size}-selected-container-shape-{round|square}--m3e-comp-icon-button-{size}-outline-width- variant container/content/selected/disabled color variables
- shared disabled opacity and focus-ring variables
The component consumes the Expressive default-effects spring already projected
by the token serializer. Theme overrides remain scoped to Material3Provider;
rendering injects no stylesheet.
Bidirectionality and SSR
All dimensions use logical inline/block properties. Icon artwork opts into RTL
mirroring through Icon; IconButton does not guess whether an image is
directional. Markup and uncontrolled initial selection are deterministic under
React server rendering and hydration.