v1.0

Button

Interactive control for primary actions, form submission, and inline triggers.

Description

Button is the primary interactive control for triggering an action. It renders as a native <button> by default, composes leading and trailing icons as children alongside a <ButtonLabel>, supports a loading state with a built-in spinner, and an asChild prop for rendering as a link or any custom element via Radix Slot.

Reach for it whenever a user performs an action: submitting a form, opening a dialog, dismissing a toast, kicking off an async operation, or navigating between pages. The variant axis maps to product intents (primary for the main action, neutral-solid / neutral-light for secondary, danger for destructive, success for confirmation, inverse pair for dark surfaces). The appearance axis controls emphasis from solid down to ghost.

Don't use Button for non-action elements. If the trigger sits inside flowing prose and reads like part of the sentence, use TextLink. If a row of related actions belongs in a single visual cluster, use ButtonGroup. If it's a dedicated dismiss affordance on a dialog or toast, use CloseButton. If it carries metadata rather than triggering an action, use Badge.

Installation

pnpm dlx @create-ui/cli add button

Usage

import { Button, ButtonLabel } from "@/components/ui/button"
<Button>Save</Button>
 
<Button>
  <RiArrowRightLine />
  <ButtonLabel>Continue</ButtonLabel>
</Button>

Examples

Variants

variant sets the semantic color intent. Seven options cover the main action (primary), neutral surfaces (neutral-solid, neutral-light), destructive (danger), confirm (success), and the inverse pair for dark backgrounds (inverse-solid, inverse-light).

Appearance

appearance controls fill weight. solid (default) is high-contrast for the main action, outline adds a border with no fill, soft uses a tinted background, and ghost drops the surface entirely until hover.

Sizes

Five sizes scale from xs (20px tall) to xl (48px). lg is the default and pairs with body type.

Shape

shape controls the radius. rounded (default) follows the size scale, pill is fully rounded, square strips the radius for tabular or chrome layouts.

With Icon

Button has no icon props — compose icons as children and wrap the text in <ButtonLabel>. The icon renders as a sibling outside the label's optical padding; the component sizes it to match the active size and handles the gap. Place the icon before <ButtonLabel> for a leading icon, after it for a trailing one. A text-only button auto-wraps, so <ButtonLabel> is only needed when an icon sits beside the text.

<Button>
  <RiAddCircleFill />
  <ButtonLabel>Leading</ButtonLabel>
</Button>
 
<Button>
  <ButtonLabel>Trailing</ButtonLabel>
  <RiArrowRightLine />
</Button>

Icon Only

iconOnly switches to a square footprint with no horizontal padding. Always pair it with an aria-label, since the icon alone is not announced by screen readers.

Loading

loading prepends a spinner before the label, sets aria-busy, and blocks pointer events so the action cannot fire twice. The label stays visible; the spinner color is picked to match the active variant and appearance. For iconOnly, the spinner replaces the icon.

As Child

asChild renders the Button styles around any child element via Radix Slot. Most commonly used to slot in an <a> (or framework Link) so a button-styled link still navigates.

Action bar

Buttons in a real settings surface: a destructive action on the left, and a primary Save (with its built-in loading state) beside a neutral Cancel on the right.

Danger zone

Delete this project and all of its data. This action cannot be undone.

Accessibility

Button is a native <button> by default and inherits its implicit button role, focus ring, and activation keys. No manual ARIA wiring is needed for the default case.

KeyDescription
SpaceActivates the button.
EnterActivates the button.
TabMoves focus to the next element.

ARIA notes:

  • loading sets aria-busy="true" on the root and disables pointer interaction; pair it with a visible label or aria-label so the busy state is announced.
  • iconOnly requires an aria-label because the icon alone has no accessible name.
  • When asChild is combined with disabled, the rendered element is not a <button> and cannot use the native disabled attribute. The component switches to aria-disabled="true" instead. If you need the element fully inert, also remove it from the tab order (tabIndex={-1}).

Styling

Tailwind override: pass className to merge Tailwind classes with the component's CVA classes (via cn()):

<Button className="tracking-wide uppercase">Save</Button>

Data slots and attributes: the component sets these for CSS targeting:

  • data-slot="button" on the root element.
  • data-slot="button-label" on the ButtonLabel text wrapper.
  • data-variant="<variant>", data-appearance="<appearance>", data-size="<size>" on the root.
  • aria-busy="true" on the root when loading. aria-disabled="true" on the root when asChild is combined with disabled.

Target a specific state in CSS:

[data-slot="button"][data-variant="primary"][data-appearance="solid"] {
  /* … */
}
  • Input Group: wraps Buttons with text inputs, selects, and addons inside a single input shell.
  • Input: the text input primitive that Buttons most often submit or filter.
  • Input Stepper: numeric input that exposes Button-styled increment and decrement triggers.
  • Inline Alert: inline status block that often pairs with a Button as its dismiss or follow-up action.

API Reference

Button

Interactive control for triggering actions. Extends React.ComponentProps<"button">, so any standard button attribute (type, id, aria-*, onClick, etc.) is accepted.

Props

PropTypeDefaultDescription
asChildbooleanfalseRender as the child element via Radix Slot. Use to slot the Button styles onto an <a> or framework link.
loadingbooleanfalseShow the loading spinner (prepended before the label), set aria-busy, and block pointer interaction.
iconOnlybooleanfalseSquare footprint with no horizontal padding; renders children as the only content. Pair with aria-label.
disabledbooleanfalseNative disabled on the <button>. Switches to aria-disabled when combined with asChild.
classNamestring-Tailwind classes merged with the component's CVA classes via cn().
childrenReact.ReactNode-Button content: a text label (auto-wrapped in ButtonLabel), icon(s) plus a <ButtonLabel>, or the icon when iconOnly. With asChild, the single child element receives the styles.

Variants

VariantOptionsDefaultDescription
variant"primary" "neutral-solid" "neutral-light" "danger" "success" "inverse-solid" "inverse-light""primary"Semantic color intent. Inverse options target dark surfaces.
appearance"solid" "outline" "soft" "ghost""solid"Fill weight. solid for emphasis, outline for a quiet border, soft tinted, ghost transparent.
size"xs" "sm" "md" "lg" "xl""lg"Size scale; controls height, padding, type, and icon size.
shape"rounded" "pill" "square""rounded"rounded follows the size scale, pill is fully rounded, square strips the radius.

ButtonLabel

Wraps a Button's text label so its optical padding stays separate from sibling icons or other content. Plain-text children are wrapped automatically; use <ButtonLabel> explicitly whenever you compose an icon (or any element) next to the text. Extends React.ComponentProps<"span">.

Props

PropTypeDefaultDescription
childrenReact.ReactNode-The label content.
classNamestring-Tailwind classes merged with the component's CVA classes via cn().

ButtonLabel reads its size from the parent Button automatically, so you normally don't set it.