# Button

> The button every screen starts with: five variants, six tones, three sizes plus icon sizes, and a loading state that never jumps.

- Type: React component (Buttons & Actions)
- Access: Free (the source is open to everyone)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add button`
- Docs: https://wingo-ui.com/components/button

## Interaction

Press it and it dips on a quick spring, then settles back; with a mouse the surface tints on hover. Tab to it for a focus ring that never touches the edge. Tap the playground button to see it load: the spinner takes the label's place and the width holds still. On a phone the medium size grows to 44px so it is easy to hit.

## Installation

```bash
npx wingo-ui@latest add button
```

The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add https://wingo-ui.com/r/button.json`

Files:

- `components/ui/button.tsx`

npm dependencies: `npm i @radix-ui/react-slot motion`

Wingo UI dependencies (installed automatically):

- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { ArrowRight } from "lucide-react"
import { Button } from "@/components/ui/button"

export function CheckoutActions({ paying, onPay }: { paying: boolean; onPay: () => void }) {
  return (
    <div className="flex gap-2">
      <Button variant="ghost">Cancel</Button>
      <Button tone="brand" loading={paying} loadingText="Paying" rightIcon={<ArrowRight />} onClick={onPay}>
        Pay now
      </Button>
    </div>
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children?` | `ReactNode` | `"Continue"` | The label. For icon sizes pass the icon itself and an aria-label. |
| `variant?` | `"solid" \| "soft" \| "outline" \| "ghost" \| "link"` | `"solid"` | solid for the one main action on a screen, soft or outline for secondary actions, ghost for toolbars and dense rows, link for actions inside text (always underlined, so it reads as a link without color or hover). |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Semantic color from the design tokens. neutral is black in light mode and white in dark mode; danger is for destructive actions. |
| `size?` | `"sm" \| "md" \| "lg" \| "icon-sm" \| "icon" \| "icon-lg"` | `"md"` | Height 32 / 40 / 48px with matching padding, type and icon size. The icon sizes are square. On touch screens md grows to 44px and sm keeps its look with a 44px hit area. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corner shape. Scales with the size (md is 8 / 10 / 12px) so every size reads as the same shape; full makes a pill. |
| `color?` | `string` | `""` | Any CSS color. Replaces the tone in every variant; text on a solid fill turns black or white by lightness. Leave empty to use the tone. |
| `loading?` | `boolean` | `false` | Shows a spinner in place of the label, keeps the width, sets aria-busy and ignores clicks. The button stays focusable, so keyboard focus is not lost. |
| `loadingText?` | `ReactNode` | `""` | Text next to the spinner while loading, like "Saving". The button reserves room for it, so it never changes width. |
| `disabled?` | `boolean` | `false` | Dims the button and blocks it. With asChild the link gets aria-disabled and leaves the tab order. |
| `fullWidth?` | `boolean` | `false` | Stretches to the container width. The usual choice for the main action at the bottom of a phone screen. |
| `asChild?` | `boolean` | `false` | Renders your single child (an <a>, a router Link) with the button's look, press and icons, instead of a <button>. The demo swaps in a link. |
| `leftIcon?` | `ReactNode` |  | An icon before the label. Unsized svgs follow the button size (14 / 16 / 18px). |
| `rightIcon?` | `ReactNode` |  | An icon after the label, like an arrow or a chevron. |
| `spinner?` | `ReactNode` |  | Replaces the default spinner. |
| `type?` | `"button" \| "submit" \| "reset"` | `"button"` | Defaults to "button" so a button inside a form never submits by accident. Set "submit" for the form's main action. |
| `onClick?` | `(event: MouseEvent<HTMLButtonElement>) => void` |  | Not called while loading or disabled. |
| `classNames?` | `Partial<Record<"content" \| "icon" \| "spinner", string>>` |  | Extra classes for the inner parts: the label wrapper, each icon wrapper, the spinner. |
| `className?` | `string` |  | Extra classes for the button itself, merged last. Override the tone vars here, e.g. [--tone-foreground:white]. |

## Examples

- **Variants and tones**: Five variants across the six tones, all driven by the design tokens.
- **Sizes**: 32, 40 and 48px, plus square icon sizes. Unsized icons follow along.
- **Loading**: The spinner takes the label's place and the width holds. loadingText gets room reserved up front.
- **Icon buttons**: Always with an aria-label. aria-pressed turns one into a toggle.
- **As a link**: asChild renders your <a> or router Link with the button's look and press.
- **Custom color**: Any CSS color. Text on the fill turns black or white on its own; soft and outline mix it for contrast.
- **Radius**: none to full; each step scales with the size.
- **Checkout on a phone**: fullWidth, lg, and a price formatted in dollars.

## See also

- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))

---

Canonical: https://wingo-ui.com/components/button
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
