Button
Labeled action control and its link twin — commit, cancel, and navigate with the same chrome.
Base UI
Button is the default labeled action control. Visible text names what happens when you activate it, and the control stays a real button for keyboard and assistive tech.
When the affordance should navigate (in-app route or external URL), use ButtonLink instead. It shares the same visual chrome but renders a native <a>, or a framework Link via render. Do not force render={<a />} on Button; Base UI keeps button semantics there.
Variants follow intent, not decoration: filled primary for the one commit action on a surface, secondary for cancel or peer choices, flat when the control should sit quietly in chrome, and destructive only when the action removes or cannot be undone. Optional addon, pill, and loading cover leading/trailing glyphs, capsule shape, and in-flight requests without jumping the layout.
When to use
- A visible text label names the action (Save, Cancel, Delete, Download)
- Footer or dialog strips where several labeled actions share a row
- Navigation that should look like a button (
ButtonLink) rather than plain text
When not to use
- Icon-only chrome with no visible label → IconButton
- Icon-only control that stays pressed → ToggleButton
- Labeled control that stays pressed → ToggleTextButton
- Tags, filters, or status pills that do not commit work → Chip
Related
- IconButton — compact icon-only actions and icon links
- ToggleButton — icon-only pressed toolbar toggles and groups
- ToggleTextButton — labeled pressed toggles and groups
- Chip — compact labels and filter pills
Common examples
Variants
Pick the variant from what the action means on that surface. A bare Button is already primary; reach for another variant when the action is not the commit.
Prefer one primary per region. Pair it with secondary cancel rather than two competing filled actions.
Addons
Pass icons through addon, not as lone children. Leading is the default; set addonPosition="end" when the glyph trails the label. Button CSS sizes SVG descendants — glyph size comes from the button, not per-icon CSS.
If the control has no visible text, switch to IconButton with an accessible name — do not empty Button children and hope the icon is enough.
Pill
pill rounds the control into a capsule. Nest it in a full-radius Surface bar when the action should read as chrome inside a strip rather than a free-floating button — put supporting copy on one side and the capsule CTA on the other so the row stays balanced.
Loading
Turn on loading while the request runs. Width stays stable so the layout does not jump when the Spinner appears; the Spinner itself stays decorative because aria-busy already marks the control. On Button, loading also blocks activation; on ButtonLink there is no disabled prop — loading sets aria-disabled / aria-busy while the link stays activatable.
Keep the same label text during the wait (or a short “…”) so the announced name stays continuous.
Action rows
Footer and dialog strips are Stack rows of Buttons — gap and alignment live on the Stack, not on per-button width CSS. Here the three actions share the strip equally: fullWidth fills the parent, expandChildren grows each Button on the main axis.
As a link
Use ButtonLink when activation should navigate. Same chrome as Button; different element — a native <a>, or a framework Link via render.
Pass href for native or external URLs (target / rel when needed). Trailing addons suit “continue” and external destinations. Pass render for in-app framework routing. Icon-only navigation belongs on IconButton (IconButtonLink).
Ask your agent
Add a footer action row to this screen with a primary Save, secondary Cancel, and destructive Delete. Show a loading state on Save while it runs. Put a leading icon on one button where it helps recognition.Last updated on