IconButton
A round button holding a single glyph. Use it where there is no room for a label: a toolbar, a list row.
tsx
import { IconButton } from 'neba';
<IconButton icon={<PlusIcon />} label="Add item" />;Props
| Prop | Type | Default | Description |
|---|---|---|---|
| icon * | ReactNode | — | The glyph. Passed bare it is sized in em against the button; wrap it in an Icon when it needs a size of its own |
| label * | string | — | What the button does, in words. The one required prop here: a button whose whole label is a drawing has no accessible name at all, and that is the single most common accessibility defect a component library ships |
| variantshared | 'solid' | 'outline' | 'text' | 'solid' | Weight of the surface: filled, hairline, or none |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Button's own height ladder, so a disc drops into a row of buttons without the row losing its baseline |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | 'default' | 'compact' | 'default' | Passed through but invisible: an icon-only control is square, so its horizontal padding is zero |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth. 0 means no shadow at all |
| loading | boolean | false | Puts a spinner in the glyph’s place and stops the button activating, while keeping it focusable |
| readOnly | boolean | false | Inert but not dimmed: the action exists, it just is not available here |
| disabled | boolean | false | Unavailable. Drops the colour family for neutral grey |
Every native <button> attribute passes through. It uses Button's axes unchanged, including variant, elevation, loading and readOnly.
For a square icon control, a Button with no children already is one.
Examples
size
The same control heights Button uses, so a disc drops into a row of buttons without the row losing its baseline.
tsx
import { Button, IconButton, Typography } from 'neba';
function SearchIcon() {
return (
<svg viewBox="0 0 16 16" fill="none">
<circle cx="7" cy="7" r="4.25" stroke="currentColor" strokeWidth="1.5" />
<path d="m10.25 10.25 3 3" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
}
const SIZES = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
/**
* The same control heights everything else uses, so a disc drops into a row of
* buttons without the row losing its baseline.
*/
export default function IconButtonSizes() {
return (
<div className="flex flex-col gap-5">
<div className="flex flex-wrap items-end gap-4">
{SIZES.map((size) => (
<div key={size} className="flex flex-col items-center gap-2">
<IconButton icon={<SearchIcon />} label={`Search, ${size}`} size={size} />
<Typography level="caption">{size}</Typography>
</div>
))}
</div>
<div className="flex flex-wrap items-center gap-2">
<Button variant="outline">Filter</Button>
<IconButton icon={<SearchIcon />} label="Search" variant="outline" />
<Typography level="caption">Same height, different shape.</Typography>
</div>
</div>
);
}loading · readOnly · disabled
They behave exactly as the same props do on Button. loading puts a spinner in place of the glyph.
tsx
import { useState } from 'react';
import { IconButton, Typography } from 'neba';
function RefreshIcon() {
return (
<svg viewBox="0 0 16 16" fill="none">
<path
d="M13 8a5 5 0 1 1-1.6-3.7"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
/>
<path
d="M13 2v3h-3"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
/** Every Button state is here unchanged, including the spinner. */
export default function IconButtonStates() {
const [loading, setLoading] = useState(false);
return (
<div className="flex flex-wrap items-center gap-4">
<div className="flex flex-col items-center gap-2">
<IconButton
icon={<RefreshIcon />}
label="Refresh"
loading={loading}
onClick={() => {
setLoading(true);
setTimeout(() => setLoading(false), 1600);
}}
/>
<Typography level="caption">press me</Typography>
</div>
<div className="flex flex-col items-center gap-2">
<IconButton icon={<RefreshIcon />} label="Refresh" disabled />
<Typography level="caption">disabled</Typography>
</div>
<div className="flex flex-col items-center gap-2">
<IconButton icon={<RefreshIcon />} label="Refresh" readOnly />
<Typography level="caption">readOnly</Typography>
</div>
<div className="flex flex-col items-center gap-2">
<IconButton icon={<RefreshIcon />} label="Refresh" elevation={2} />
<Typography level="caption">elevation 2</Typography>
</div>
</div>
);
}label
label is a required prop. A button whose only content is a glyph has no other way to get an accessible name, so the type demands one.
tsx
// Type error.
<IconButton icon={<TrashIcon />} />
// This.
<IconButton icon={<TrashIcon />} label="Delete file" />label is not shown on screen. Wrap the button in a Tooltip to make the name visible too.
Accessibility
labelis passed through asaria-label.- The focus ring only appears on
:focus-visible.