Browse docs
Browse docs
Trigger an action with an icon. Compact. Accessible by design.
import { IconButton } from '@dashforge/tw';
import { Pencil } from 'lucide-react';
<IconButton aria-label="Edit" color="primary">
<Pencil size={16} />
</IconButton><IconButton aria-label="Save" color="primary"><Save /></IconButton>
<IconButton aria-label="Delete" color="danger" variant="outline">
<Trash2 />
</IconButton>
<IconButton aria-label="Refresh" loading={isFetching}>
<RefreshCw />
</IconButton>Required prop: aria-label is enforced at the TypeScript level — TS rejects the component if you omit it. Icon-only buttons MUST have an accessible label.
Shares the same variant axis as <Button>: solid · outline · ghost · link.
import { IconButton, Stack } from '@dashforge/tw';
import { Pencil } from 'lucide-react';
<Stack direction="row" gap={2} align="center">
<IconButton aria-label="Edit" variant="solid"><Pencil size={16} /></IconButton>
<IconButton aria-label="Edit" variant="outline"><Pencil size={16} /></IconButton>
<IconButton aria-label="Edit" variant="ghost"><Pencil size={16} /></IconButton>
<IconButton aria-label="Edit" variant="link"><Pencil size={16} /></IconButton>
</Stack>Square hit areas with no horizontal padding (the icon sits centered):
import { IconButton, Stack } from '@dashforge/tw';
import { Star } from 'lucide-react';
<Stack direction="row" gap={2} align="center">
<IconButton aria-label="Star" size="sm" color="warning"><Star size={14} /></IconButton>
<IconButton aria-label="Star" size="md" color="warning"><Star size={16} /></IconButton>
<IconButton aria-label="Star" size="lg" color="warning"><Star size={18} /></IconButton>
</Stack>Semantic intent palette identical to Button:
<IconButton aria-label="Primary" color="primary"><Star /></IconButton>
<IconButton aria-label="Secondary" color="secondary"><Star /></IconButton>
<IconButton aria-label="Success" color="success"><Check /></IconButton>
<IconButton aria-label="Warning" color="warning"><AlertTriangle /></IconButton>
<IconButton aria-label="Danger" color="danger"><Trash2 /></IconButton>
<IconButton aria-label="Info" color="info"><Info /></IconButton>
<IconButton aria-label="Neutral" color="neutral"><Settings /></IconButton>The loading prop swaps the icon for a <Spinner> (Sprint 4.4 DRY — both <Button> and <IconButton> delegate the spinner glyph to the standalone <Spinner> component). Clicks are short-circuited while loading.
import { useState } from 'react';
import { IconButton } from '@dashforge/tw';
import { RefreshCw } from 'lucide-react';
function RefreshButton() {
const [loading, setLoading] = useState(false);
return (
<IconButton
aria-label="Refresh"
color="primary"
loading={loading}
onClick={() => {
setLoading(true);
setTimeout(() => setLoading(false), 1800);
}}
>
<RefreshCw size={16} />
</IconButton>
);
}<IconButton aria-label="Disabled" disabled>
<Pencil />
</IconButton>access (RBAC) and visibleWhen (engine-reactive) are available — the universal Sprint 4.4 contract that every interactive component ships.
<IconButton aria-label="Delete record" color="danger"
access={{ requires: 'records.delete', when: 'denied:hide' }}
>
<Trash2 />
</IconButton>
<IconButton aria-label="Restore" color="info"
visibleWhen={(engine) => engine.getValue('status') === 'archived'}
>
<Undo2 />
</IconButton>asChildRender the IconButton styling onto another element (router <Link>, anchor, etc.) via Radix Slot. The TS aria-label requirement still applies.
<IconButton asChild aria-label="Open settings page" variant="ghost">
<Link to="/settings"><Settings /></Link>
</IconButton>Configure <IconButton> defaults application-wide. Shares the buttonVariants recipe with <Button> — the same variant axes apply.
import { patchTheme } from '@dashforge/tw-theme';
patchTheme({
components: {
IconButton: {
defaults: { color: 'neutral', variant: 'ghost', size: 'md' },
},
},
});Configurable axes (IconButtonVariantProps):
| Axis | Type | Notes |
|---|---|---|
variant | 'solid' | 'outline' | 'ghost' | 'link' | Visual treatment. |
color | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | Semantic intent. |
size | 'sm' | 'md' | 'lg' | Square dimensions matching Button height. |
loading | boolean | Replace icon with spinner. Rarely a useful theme default. |
Non-visual axes (aria-label, disabled, asChild, access, visibleWhen, event handlers) are not theme-configurable — required per-instance semantics.
Precedence chain (lowest → highest):
defaultVariants from the shared buttonVariants recipe.theme.components.IconButton.defaults (application-wide).<IconButton color="danger" />) — wins over theme.sx — appended to the root and wins over conflicting classes via tailwind-merge.Note: because IconButton reuses buttonVariants, setting theme.components.Button.defaults does not affect IconButton — they have independent theme entries. This is intentional (IconButton often deserves a subtler default variant like ghost).
TypeScript: theme.components.IconButton.defaults autocompletes to the four axes above.
Reactivity: useComponentDefaults('IconButton') subscribes to the theme store — patchTheme re-renders every mounted instance inheriting the changed axis.
| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | — | Required at the type level — icon-only buttons MUST have an accessible name. Pass a short verb phrase describing the action (e.g. "Delete row", "Open settings", NOT "Trash icon"). |
children | ReactNode | — | The icon — typically an SVG element from the consumer's icon library (Lucide / Phosphor / Tabler / inline SVG). A single ReactNode is expected. |
access | AccessRequirement | — | RBAC requirement. When the current subject doesn't satisfy the requirement, the button is hidden / disabled / read-only per onUnauthorized. Same contract as <Button access>. |
asChild | boolean | — | Render-as-child via Radix Slot — the resolved IconButton className is forwarded to the immediate child element, no <button> DOM is emitted. Useful for router <Link>s. Same contract as <Button asChild>. |
color | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'neutral' | Semantic intent role — same axis as <Button>, though the default here is 'neutral' (icon-only surfaces usually deserve a subtler default variant than solid + primary). |
loading | boolean | false | When true, replace the icon with a <Spinner>, short-circuit clicks, and set aria-busy="true". |
size | 'md' | 'sm' | 'lg' | 'md' | Square hit-area tier — matches Button's height scale so a <Button size="md"> and <IconButton size="md"> align in the same row. |
sx | ClassValue | — | Root-element class shortcut (string or clsx-compatible value). Last-wins via tailwind-merge inside cn(). |
variant | 'solid' | 'link' | 'outline' | 'ghost' | 'ghost' | Visual treatment. Same axis as <Button>; default is 'ghost' because icon-only actions are typically secondary. |
visibleWhen | (engine: Engine) => boolean | — | Reactive visibility predicate — same contract as <Button> and <Chip>. When the predicate returns false, the component renders null. Inside a <DashForm> it subscribes to engine state and re-evaluates on every change; outside a form, it's called with null engine (capture external state in closure).Added in 1.1.0 (Sprint 4.4 alignment) to keep IconButton in lockstep with Button. |
...rest | Omit< ButtonHTMLAttributes<HTMLButtonElement>, 'color' | 'children' > | — | Additional native attributes forwarded via Omit< ButtonHTMLAttributes<HTMLButtonElement>, 'color' | 'children' >. |
<IconButton> has no slots. It renders a single square <button> DOM element with an icon child and an optional loading <Spinner> — there is no anatomy to target separately.
The slotProps API pattern only exists on compound components with multiple visually distinct regions. Examples: <Menu> (root, trigger, content, item), <DataGrid>, <Autocomplete>.
For <IconButton>, use sx for any style override. If you need more surface than that, asChild lets you render the IconButton's classes onto an arbitrary element you fully control.
iconButtonVariants is exported for advanced consumers who want to apply the styling outside the React tree (e.g. inside a custom Radix Trigger).
import { iconButtonVariants } from '@dashforge/tw';
<button className={iconButtonVariants({ color: 'primary', size: 'md' })}>
<Star />
</button><Spinner> component — <Button> and <IconButton> both delegate to it. Visual consistency guaranteed.size prop. IconButton doesn't auto-size the child; that would conflict with the icon library's own sizing semantics.aria-label is the strongest a11y enforcement in the catalog. See Design decisions for the rationale.