Overview
<Button /> is the canonical interactive primitive. Use it to trigger an action — submit a form,
open a dialog, advance a flow, etc. Buttons are intentionally stateful (loading, disabled) and
polymorphic (asChild), so they cover everything from a plain submit to a styled <a> link.
Overview — variant × color × icon at a glance
Variants
Three stylistic families ship out of the box. Pick the one that matches the action's importance hierarchy on the page:
solid— opaque fill, contrast text. The primary call-to-action. Use one per region.outline— transparent fill, 1px colored border, colored text. Secondary actions ("Cancel" next to "Save"). Pairs gracefully with a solid sibling without competing for attention.ghost— no fill, no border, colored text. Tertiary / in-line / utility actions (toolbar buttons, "Show more", inline edits). The least visually heavy variant.
Every variant works with every color and every theme variant (default / tetsu / origami
/ katana) and adapts to the active platform. All hover, active, and focus states reference
semantic palette tokens, so a single Theme-Studio edit re-paints the entire grid.
Anatomy
A Button is composed of three optional slots arranged inline:
[ leftIcon | label | rightIcon ][ leftIcon | label | rightIcon ]When loading is true, the left slot is replaced by a spinning indicator and the label is
optionally swapped with loadingText for assistive tech.
Examples
Basic
Variants
Sizes
Colors
OutlineColors
GhostColors
VariantMatrix
WithIcons
Loading
Disabled
FullWidth
AsChild
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| asChild | boolean | — | Radix-style polymorphism. When `true`, Button merges its props/className/ref onto the single child element (e.g. wrap an `<a>` to render as a styled link). |
| color | ResponsiveValue<ButtonColor> | 'primary' | Semantic palette role driving fill / text / focus-ring colors. |
| fullWidth | ResponsiveValue<boolean> | false | Stretch to fill the parent's inline-size. |
| iconOnly | boolean | — | Force the square icon-only layout. Inferred automatically when `children` is empty and at least one of `leftIcon` / `rightIcon` is provided. Requires `aria-label`. |
| leftIcon | ReactNode | — | Element rendered before the label (e.g. an icon). Visually swaps with `rightIcon` in RTL. |
| loading | boolean | — | When `true`, blocks interaction, sets `aria-busy`, and shows a spinner. |
| loadingText | string | — | Optional label rendered (and announced) in place of `children` while loading. |
| rightIcon | ReactNode | — | Element rendered after the label. |
| size | ResponsiveValue<ButtonSize> | 'md' | Visual height + horizontal padding. |
| sx | Sx | — | Theme-aware inline style object (resolves palette / spacing / radius tokens to CSS vars). |
| variant | ResponsiveValue<ButtonVariant> | 'solid' | Stylistic family. |
Accessibility
- Renders a native
<button>by default — Space and Enter activate, focus is keyboard-visible. disabledandloadingboth setaria-disabledand blockonClick. While loading, the button also getsaria-busy="true"and acursor-progresshint.- The spinner is
role="status"with anaria-labelofloadingText ?? "Loading". asChildlets you wrap any element (an<a>, a router<Link>, …) — Button hands all props/className/ref throughSlotso the child keeps its native role.iconOnly(explicit or inferred whenchildrenis empty) requiresaria-label. A dev-mode warning fires from the engine when it's missing.
Theming
Every visual decision is a token. Pull on the <ThemeProvider> knobs to remix the whole library
at once, or scope overrides to Button specifically:
<ThemeProvider
theme={defineTheme({
components: {
Button: {
styleOverrides: {
root: 'tracking-wide uppercase',
},
},
},
})}
>
{children}
</ThemeProvider><ThemeProvider
theme={defineTheme({
components: {
Button: {
styleOverrides: {
root: 'tracking-wide uppercase',
},
},
},
})}
>
{children}
</ThemeProvider>Component-level overrides win via tailwind-merge:
<Button className="border-2 border-dashed" sx={{ radius: 'lg' }} style={{ minWidth: 200 }}>
Custom
</Button><Button className="border-2 border-dashed" sx={{ radius: 'lg' }} style={{ minWidth: 200 }}>
Custom
</Button>The override ladder is fully documented on the Theming page.
Do / Don't
- Do name the action ("Save changes", not "OK"). Verbs > nouns.
- Do pair destructive actions with
color="danger"so the palette tells the same story as the label. - Don't use Button for navigation — wrap an
<a>withasChildinstead, so the element remains semantically a link. - Don't disable a button without telling the user why; pair
disabledwith a tooltip or helper text whenever feasible.