Browse docs
Browse docs
Trigger an action. Submit a form. Render as anything else with asChild.
import { Button } from '@dashforge/tw';
<Button color="primary">Save changes</Button><Button color="primary" size="md" onClick={handleSave}>
Save changes
</Button>
<Button variant="outlined" startIcon={<DownloadIcon />}>
Export
</Button>
<Button color="danger" variant="text" loading={isDeleting}>
Delete
</Button>State (loading, disabled) and visual (variant, color, size) are all props — never a utility chain on the consumer side.
import { Button, Stack } from '@dashforge/tw';
<Stack direction="row" gap={2} wrap align="center">
<Button variant="solid">Solid</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
</Stack><Button variant="solid" color="primary">Solid</Button>
<Button variant="outline" color="primary">Outline</Button>
<Button variant="ghost" color="primary">Ghost</Button>
<Button variant="link" color="primary">Link</Button>Solid for primary actions, outlined for secondary, text for tertiary or inline, ghost for hover-revealed surfaces.
import { Button, Stack } from '@dashforge/tw';
<Stack direction="row" gap={2} wrap align="center">
<Button color="primary">Primary</Button>
<Button color="secondary">Secondary</Button>
<Button color="success">Success</Button>
<Button color="warning">Warning</Button>
<Button color="danger">Danger</Button>
</Stack><Button color="primary"> Primary</Button>
<Button color="secondary">Secondary</Button>
<Button color="success"> Success</Button>
<Button color="warning"> Warning</Button>
<Button color="danger"> Danger</Button>Use semantic colors for semantic actions (danger for destructive, success for confirmation). neutral for visual de-emphasis without losing affordance.
import { Button, Stack } from '@dashforge/tw';
<Stack gap={3} align="start">
<Stack direction="row" gap={2} align="center">
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
</Stack>
<Button fullWidth>Full width</Button>
</Stack><Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
<Button fullWidth>Full width</Button>const [saving, setSaving] = useState(false);
<Button
color="primary"
loading={saving}
onClick={async () => {
setSaving(true);
await save();
setSaving(false);
}}
>
Save
</Button>While loading is true the button is disabled, the label is replaced by a spinner, the click handler is short-circuited. No flash of double-submits.
<Button startIcon={<PlusIcon />} color="primary">Add item</Button>
<Button endIcon={<ArrowRightIcon />}>Next</Button>
<Button startIcon={<PlusIcon />} aria-label="Add" /> {/* icon-only */}Icon-only buttons MUST set aria-label for screen readers — the linter flags missing labels.
asChild — render as a link, or anythingimport { Link } from 'react-router-dom';
<Button asChild color="primary">
<Link to="/dashboard">Open dashboard</Link>
</Button>asChild merges the Button's styles + behavior onto its single child element via Radix's Slot pattern. The actual DOM element is the child — useful for client-side routing without losing button styling.
<Button color="danger" access={{ requires: 'workspace.delete', when: 'denied:hide' }}>
Delete workspace
</Button>Three modes via the when field: 'denied:hide' (default — disappears), 'denied:disable' (greys out), 'denied:readonly' (clickable label, but the underlying action is short-circuited at the bridge). Shared semantics with the MUI side. See Access Control.
import { DashForm } from '@dashforge/forms';
import { TextField, Button } from '@dashforge/tw';
<DashForm onSubmit={signIn}>
<TextField name="email" required />
<Button type="submit" color="primary">Sign in</Button>
</DashForm>type="submit" integrates with <DashForm> — the button enters loading state automatically while the submit handler is pending.
| Prop | Type | Default | Description |
|---|---|---|---|
color | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'neutral' | 'neutral' | Semantic intent. Drives both bg/fg and hover/active states. |
variant | 'solid' | 'outlined' | 'text' | 'ghost' | 'solid' | Visual treatment. |
size | 'sm' | 'md' | 'lg' | 'md' | Height + padding + font size. |
loading | boolean | false | Replace label with spinner + short-circuit clicks. |
disabled | boolean | false | Disable the underlying <button>. |
fullWidth | boolean | false | Stretch to container width. |
startIcon | ReactNode | — | Icon rendered before the label. |
endIcon | ReactNode | — | Icon rendered after the label. |
asChild | boolean | false | Render styles onto child element (Radix Slot). |
access | AccessSpec | — | RBAC gating. See Access Control. |
className | string | — | Merged via tailwind-merge. Your classes win. |
slotProps | { icon?, label? } | — | Per-slot overrides. |
| ...rest | React.ButtonHTMLAttributes | — | All native button props pass through. |
icon (wrapper around startIcon/endIcon) · label (the inline text).
The full tailwind-variants definition lives in the package source — every variant + color + size combination is enumerable, useful when extending the catalogue downstream:
import { buttonVariants } from '@dashforge/tw';
// buttonVariants({ color: 'primary', size: 'md', variant: 'solid' }) → className stringfocus-visible (keyboard only), respecting :focus-visible support.text and ghost variants is a subtle background, not a transform — preserves baseline rhythm.aria-hidden; the button itself sets aria-busy="true".<Button> is <button type="button"> by default — explicitly set type="submit" when needed inside forms.