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>| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | (required) | Accessible name. Enforced by TypeScript — omitting it is a compile error. |
color | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'neutral' | 'neutral' | Semantic intent. |
variant | 'solid' | 'outline' | 'ghost' | 'link' | 'solid' | Visual treatment. Same axis as Button. |
size | 'sm' | 'md' | 'lg' | 'md' | Square hit area — sm: 32×32, md: 40×40, lg: 48×48. |
loading | boolean | false | Replace icon with Spinner + short-circuit clicks. |
disabled | boolean | false | Disable underlying <button>. |
asChild | boolean | false | Render onto child element via Radix Slot. |
access | AccessSpec | — | RBAC gating. See Access Control. |
visibleWhen | (engine: Engine) => boolean | — | Engine-reactive predicate. Returns null when false. |
className | string | — | Merged via tailwind-merge. |
sx | string | — | String-form Tailwind override. |
| ...rest | React.ButtonHTMLAttributes | — | All native button props pass through. |
slotProps. IconButton renders a single DOM element with no sub-slots — className + sx cover the override surface fully. The full slot system is reserved for <Button> (which has icon + label slots) and other multi-slot components.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.