Button
Button styled by a tone × emphasis grammar: tone sets the semantic colour
(neutral / brand / danger) and emphasis sets the visual weight (solid / soft /
outline / ghost / transparent / link). Any pairing is valid — there is no fixed
"primary/secondary" list; those looks are expressed as tone × emphasis pairs.
An icon-only button (an icon-* size, or icon with no children) requires an
aria-label; a dev-only console warning fires when it is missing. With asChild,
styling and icon/dot decorations are merged onto the single child via Slot.
Usage
@desk/ui/components/button
<Flex align="center" gap="sm" justify="space-between" wrap="wrap">
<Group gap="sm">
<Button icon={Package} tone="brand">
Request sample
</Button>
<Button emphasis="outline" icon={Heart} tone="neutral">
Save to palette
</Button>
<Button
aria-label="Share"
emphasis="ghost"
icon={Share2}
tone="neutral"
/>
</Group>
<Button emphasis="ghost" icon={Trash2} tone="danger">
Remove
</Button>
</Flex>Best practices
Express weight with
tone×emphasis, not avariantprop — there is no "primary/secondary"; e.g. the destructive confirm istone="danger".Icon-only buttons MUST have an
aria-label(a dev warning fires otherwise).Use
asChildto render a link/Slot child (e.g. Next<Link>) rather than nesting a button inside an anchor.Reach for
emphasis="ghost"/"link"in toolbars/headers — they carrydata-edge-compso containers can pull them flush to the edge.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| tone | "neutral" | "brand" | "danger" | null | undefined | ||
| radius | "rounded" | "square" | "full" | null | undefined | ||
| square | boolean | null | undefined | ||
| emphasis | "link" | "outline" | "solid" | "ghost" | "soft" | "transparent" | null | undefined | ||
| asInput | boolean | null | undefined | ||
| align | "center" | "end" | "start" | null | undefined | ||
| pressed | boolean | null | undefined | ||
| size | ButtonSizeProp | "base" | Size token; |
| asChild | boolean | undefined | false | Merge props and styling onto the single child element via Slot. |
| loading | boolean | undefined | false | Show a spinner and disable interaction. |
| loadingLabel | string | undefined | Label shown in place of | |
| icon | IconComponent | undefined | Leading icon (Lucide or SVG component); with no children it becomes an icon-only button. | |
| iconClassName | string | undefined | Extra classes for the leading icon. | |
| suffixIcon | IconComponent | undefined | Trailing icon (Lucide or SVG component) rendered at the end. | |
| suffixIconClassName | string | undefined | Extra classes for the trailing icon. | |
| dot | string | undefined | CSS colour for a status dot; empty/undefined renders no dot. | |
| dotPlacement | "end" | "start" | undefined | "start" | Dot position relative to content. |
| expandable | boolean | undefined | false | Icon-only at rest; expands on hover/focus-visible to reveal the label
to the right of the icon. Collapsed geometry equals the matching icon-*
size, expanded equals the text button of that size — so pass a text
size ( |
| focused | boolean | undefined | Force the focus ring on — for documentation/testing only. | |
| hovered | boolean | undefined | Force the hover state on — for documentation/testing only. | |
| testId | string |
Also exports
- const
BUTTON_EMPHASES - const
BUTTON_TONES - type
ButtonEmphasis - type
ButtonProps - type
ButtonSize - type
ButtonTone - const
buttonVariants
Composition
Used by
AlertDialog, Calendar, ColorPalette, Combobox, DataGrid, DataGridColumnFilter, DataGridColumnHeader, DataGridPagination, ErrorState, FileUploadDropzone, InputGroup, MobileNavigationRoot, Pagination, ProductCard, PromptPanel, QuantityControl, Select, SettingsCard, Sidebar, Sidepanel, LiveWaveform