---
title: Button
description: Action button with intent + surface composition and loading state
visibility: guest
draft: false
related:
  - /components/link
  - /components/badge
  - /components/callout
---

# Button

A flexible button component composed from `intent` (semantic color) and `surface` (visual style), with sizes, icons, and a loading state.

## Live Sandbox

The "Live preview" toggle on the block below swaps the static rendered HTML for a hydrated Livewire instance — every prop in the schema becomes editable from the iframe and the result re-renders server-side. Toggle off to return to the static preview.

:::preview{title="Sandbox" sandbox="button" props='{"intent":"primary","surface":"filled","body":"Save"}'}
<x-wirekit::button intent="primary">Save</x-wirekit::button>
:::

## Basic Usage

:::preview{title="Common shapes"}
<x-wirekit::row gap="xs" :wrap="true">
    <x-wirekit::button>Default</x-wirekit::button>
    <x-wirekit::button intent="neutral">Secondary</x-wirekit::button>
    <x-wirekit::button intent="neutral" surface="outline">Outline</x-wirekit::button>
    <x-wirekit::button intent="neutral" surface="ghost">Ghost</x-wirekit::button>
    <x-wirekit::button intent="danger">Danger</x-wirekit::button>
    <x-wirekit::button surface="link">Link</x-wirekit::button>
</x-wirekit::row>
:::

## Sizes

:::preview{title="Sizes"}
<x-wirekit::button size="xs">Extra Small</x-wirekit::button>
<x-wirekit::button size="sm">Small</x-wirekit::button>
<x-wirekit::button size="md">Medium</x-wirekit::button>
<x-wirekit::button size="lg">Large</x-wirekit::button>
<x-wirekit::button size="xl">Extra Large</x-wirekit::button>
:::

## As Link

:::preview{title="A button that is really a link"}
<x-wirekit::button href="/dashboard">Go to Dashboard</x-wirekit::button>
:::

Renders as `<a>` instead of `<button>`.

## Loading State

```blade
<x-wirekit::button loading wire:click="save">Save changes</x-wirekit::button>
```

The `loading` prop adds a spinner that is visible **only during Livewire requests** (via `wire:loading`). The button is also automatically disabled while loading to prevent double-clicks.

On a page that polls (`wire:poll`) or has several action buttons, add `loading-target` so the spinner only reacts to this button's own action instead of flashing on every commit:

```blade
<x-wirekit::button loading wire:click="redeliver('{{ $id }}')" loading-target="redeliver">Retry</x-wirekit::button>
```

The preview below simulates that behavior with Alpine — clicking the button flips into a 2-second "saving" state so you can see the spinner + disabled treatment without a Livewire backend:

:::preview{title="Click to simulate a Livewire request"}
<x-wirekit::row x-data="{ saving: false }" gap="md" align="center">
    <x-wirekit::button
        @click="saving = true; setTimeout(() => saving = false, 2000)"
        x-bind:disabled="saving"
    >
        <span x-show="!saving" style="white-space: nowrap;">Save changes</span>
        <span x-show="saving" style="white-space: nowrap;">
            <svg style="display: inline-block; vertical-align: middle; width: 1rem; height: 1rem; margin-right: var(--gap-wk-sm); animation: wk-progress-circle-spin 1s linear infinite;" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" style="opacity: 0.25;"></circle>
                <path d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" fill="currentColor" style="opacity: 0.75;"></path>
            </svg><span style="display: inline-block; vertical-align: middle;">Saving&hellip;</span>
        </span>
    </x-wirekit::button>
    <x-wirekit::text size="sm" variant="muted">Click the button.</x-wirekit::text>
</x-wirekit::row>
:::

## Icons

### With WireKit Icon Component

Use the `iconLeft` or `iconRight` slots together with the [Icon component](/components/icon):

:::preview{title="Buttons with icon aliases"}
<x-wirekit::button>
    <x-slot:iconLeft><x-wirekit::icon name="edit" class="w-4 h-4" /></x-slot:iconLeft>
    Edit
</x-wirekit::button>

<x-wirekit::button intent="danger">
    <x-slot:iconLeft><x-wirekit::icon name="trash" class="w-4 h-4" /></x-slot:iconLeft>
    Delete
</x-wirekit::button>

<x-wirekit::button intent="neutral" surface="outline">
    Next
    <x-slot:iconRight><x-wirekit::icon name="chevron-right" class="w-4 h-4" /></x-slot:iconRight>
</x-wirekit::button>

<x-wirekit::button intent="neutral" surface="ghost">
    <x-slot:iconLeft><x-wirekit::icon name="download" class="w-4 h-4" /></x-slot:iconLeft>
    Download
</x-wirekit::button>
:::

### With Custom SVG

You can also pass raw SVGs for full control — use the `iconLeft` / `iconRight` slots and size the SVG with an inline `style="width: 1rem; height: 1rem;"` (1rem = Tailwind `w-4 h-4`) so the glyph matches the default icon sizing:

:::preview{title="Single button with raw SVG"}
<x-wirekit::button intent="neutral">
    <x-slot:iconLeft>
        <svg style="width: 1rem; height: 1rem;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
            <path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm.75-11.25a.75.75 0 0 0-1.5 0v2.5h-2.5a.75.75 0 0 0 0 1.5h2.5v2.5a.75.75 0 0 0 1.5 0v-2.5h2.5a.75.75 0 0 0 0-1.5h-2.5v-2.5Z" clip-rule="evenodd" />
        </svg>
    </x-slot:iconLeft>
    Add Item
</x-wirekit::button>
:::

Mix the WireKit `<x-wirekit::icon>` component with raw SVGs in the same button row — the two sizing approaches (`class="w-4 h-4"` on the icon component vs `style="width: 1rem; height: 1rem;"` on the raw SVG) render identically, so they can be used side by side:

:::preview{title="Icon component and raw SVG side by side"}
<x-wirekit::button>
    <x-slot:iconLeft><x-wirekit::icon name="edit" class="w-4 h-4" /></x-slot:iconLeft>
    Edit
</x-wirekit::button>

<x-wirekit::button intent="neutral">
    <x-slot:iconLeft>
        <svg style="width: 1rem; height: 1rem;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
            <path d="M15.988 3.012A2.25 2.25 0 0 0 14.174 2h-8.35A2.25 2.25 0 0 0 3.612 3.489l-2.5 6A2.25 2.25 0 0 0 .875 10.5v4.75A2.25 2.25 0 0 0 3.125 17.5h13.75a2.25 2.25 0 0 0 2.25-2.25V10.5a2.25 2.25 0 0 0-.238-1.011l-2.5-6a2.25 2.25 0 0 0-.399-.477ZM10 12.75a2.25 2.25 0 1 0 0-4.5 2.25 2.25 0 0 0 0 4.5Z" />
        </svg>
    </x-slot:iconLeft>
    Save
</x-wirekit::button>

<x-wirekit::button intent="neutral" surface="outline">
    Next
    <x-slot:iconRight><x-wirekit::icon name="chevron-right" class="w-4 h-4" /></x-slot:iconRight>
</x-wirekit::button>
:::

## Intent × Surface

The button is composed from two orthogonal props: `intent` selects the semantic color (`primary`, `neutral`, `success`, `warning`, `danger`, `info`) and `surface` selects the visual style (`filled`, `outline`, `soft`, `ghost`, `link`). Any combination is valid — the same color reads as a strong action when filled, a neutral chrome when outlined, and a tertiary affordance when soft or ghost.

:::preview{title="Primary intent across surfaces"}
<x-wirekit::button intent="primary" surface="filled">Filled</x-wirekit::button>
<x-wirekit::button intent="primary" surface="outline">Outline</x-wirekit::button>
<x-wirekit::button intent="primary" surface="soft">Soft</x-wirekit::button>
<x-wirekit::button intent="primary" surface="ghost">Ghost</x-wirekit::button>
<x-wirekit::button intent="primary" surface="link">Link</x-wirekit::button>
:::

:::preview{title="Danger intent across surfaces"}
<x-wirekit::button intent="danger" surface="filled">Delete</x-wirekit::button>
<x-wirekit::button intent="danger" surface="outline">Remove</x-wirekit::button>
<x-wirekit::button intent="danger" surface="soft">Reject</x-wirekit::button>
<x-wirekit::button intent="danger" surface="ghost">Cancel</x-wirekit::button>
:::

:::preview{title="All intents with filled surface"}
<x-wirekit::row gap="xs" :wrap="true">
    <x-wirekit::button intent="primary" surface="filled">Primary</x-wirekit::button>
    <x-wirekit::button intent="neutral" surface="filled">Neutral</x-wirekit::button>
    <x-wirekit::button intent="success" surface="filled">Success</x-wirekit::button>
    <x-wirekit::button intent="warning" surface="filled">Warning</x-wirekit::button>
    <x-wirekit::button intent="danger" surface="filled">Danger</x-wirekit::button>
    <x-wirekit::button intent="info" surface="filled">Info</x-wirekit::button>
</x-wirekit::row>
:::

## Button Group

Wrap buttons in `<x-wirekit::button.group>` to join them into a single
segmented control — inner corners are squared, the shared border seam collapses
to one line, and the active button rises above the seam. Add `label` for the
group's accessible name, and `orientation="vertical"` to stack instead.

:::preview{title="Joined button group"}
<x-wirekit::button.group label="Date range">
    <x-wirekit::button intent="neutral" surface="outline">Day</x-wirekit::button>
    <x-wirekit::button intent="neutral" surface="outline">Week</x-wirekit::button>
    <x-wirekit::button intent="neutral" surface="outline">Month</x-wirekit::button>
</x-wirekit::button.group>
:::

:::preview{title="Vertical button group"}
<x-wirekit::button.group orientation="vertical" label="Actions">
    <x-wirekit::button intent="neutral" surface="outline">Edit</x-wirekit::button>
    <x-wirekit::button intent="neutral" surface="outline">Duplicate</x-wirekit::button>
    <x-wirekit::button intent="neutral" surface="outline">Archive</x-wirekit::button>
</x-wirekit::button.group>
:::

The same wrapper joins an input with a trailing button — an attached
search field, a newsletter signup, or a copy-link row:

:::preview{title="Input with attached button"}
<x-wirekit::button.group label="Search" class="w-full">
    <x-wirekit::input type="search" aria-label="Search projects" placeholder="Search projects…" class="flex-1" />
    <x-wirekit::button>Search</x-wirekit::button>
</x-wirekit::button.group>
:::

## Width & Layout

Buttons auto-size to their content by default. Use Tailwind classes for full-width or fixed-width buttons:

```blade
{{-- Full-width button --}}
<x-wirekit::button class="w-full">Submit</x-wirekit::button>

{{-- Fixed width --}}
<x-wirekit::button class="w-48">Save Changes</x-wirekit::button>
```

Height is controlled by the `size` prop (`xs`, `sm`, `md`, `lg`).

::: info Touch targets
`xs` (~28px tall) sits below the ~44px comfortable touch-target size. Reach for it in
dense desktop UIs — toolbars, table-row actions, filter chips — not for a primary
action a finger has to hit on a phone, where `md` or larger keeps the target roomy.
:::

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `intent` | string | `'primary'` | Semantic intent: `primary`, `neutral`, `success`, `warning`, `danger`, `info` |
| `surface` | string | `'filled'` | Visual surface: `filled`, `outline`, `soft`, `ghost`, `link` |
| `size` | string | `'md'` | `xs`, `sm`, `md`, `lg`, `xl` |
| `type` | string | `'button'` | HTML button type |
| `href` | string\|null | `null` | Renders as `<a>` when set |
| `disabled` | bool | `false` | Disables the button |
| `loading` | bool | `false` | Shows spinner during Livewire requests (auto-detects `wire:click` context) |
| `loadingTarget` | `string\|null` | `null` | Scopes the spinner + disable to this button's own Livewire action (e.g. `loading-target="redeliver"`). Without it, an untargeted `wire:loading` flashes the spinner on every commit — including `wire:poll` refreshes and unrelated actions. Opt-in; omit for the current untargeted behavior. |
| `forceLoading` | bool | `false` | Renders the spinner unconditionally and disables the button regardless of any `wire:loading` gate. Useful for static documentation demos, non-Livewire contexts, or stories that want to preview the loading state without a backend request in flight. |
| `scope` | string\|null | `null` | Scoped personalization name |

### `<x-wirekit::button.group>`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `orientation` | string | `'horizontal'` | `'horizontal'` (join left-to-right) or `'vertical'` (stack) |
| `label` | string\|null | `null` | Accessible name for the group (`role="group"`) |
| `scope` | string\|null | `null` | Scoped personalization name |

## Slots

| Slot | Description |
| --- | --- |
| default | Button label text |
| `iconLeft` | Content rendered before the label (e.g. SVG icon) |
| `iconRight` | Content rendered after the label (e.g. SVG icon) |

## Accessibility

The button component uses native `<button>` semantics (or `<a>` when `href` is set), so browsers and assistive technology handle roles, focus, and activation automatically.

- **`type="button"`** — Every `<button>` renders with `type="button"` by default, preventing accidental form submission. Override with `type="submit"` when needed.
- **Disabled state** — The `disabled` prop sets the native `disabled` attribute. The component applies `opacity-[var(--opacity-wk-disabled)]` and `pointer-events-none` so the button is visually muted and unreachable by pointer.
- **Focus ring** — Uses `focus-visible` (not `focus`) so the ring only appears for keyboard navigation, never for mouse clicks.
- **Loading state** — When `loading` is set, the button is automatically disabled during Livewire requests via `wire:loading.attr="disabled"`, preventing double submissions. The spinner SVG includes `aria-hidden="true"`.
- **Icon-only buttons** — When a button has no visible text label (icon only), add an `aria-label` attribute:

  ```blade
  <x-wirekit::button intent="neutral" surface="ghost" aria-label="Close">
      <x-slot:iconLeft><x-wirekit::icon name="close" class="w-4 h-4" /></x-slot:iconLeft>
  </x-wirekit::button>
  ```

- **External links** — When `href` and `target="_blank"` are used together, the component automatically injects `rel="noopener noreferrer"` (prevents tabnabbing) and a screen-reader hint "(opens in new tab)".
- **Keyboard** — `Enter` and `Space` activate the button (native browser behavior). No custom key handlers needed.

## Keyboard Interaction

| Key | Action |
|-----|--------|
| `Tab` | Move focus to the button |
| `Enter` / `Space` | Activate the button |

## Pitfalls

- **Don't omit `type="button"` inside a `<form>`.** The HTML default is `submit` — clicking the button will submit the surrounding form even if you wired a `wire:click` handler. The component already injects `type="button"` for non-submit usage, but if you splat `{{ $attributes }}` over a wrapper, double-check the rendered HTML.
- **Don't hardcode `class="bg-..."` to recolor.** Use `intent="primary|neutral|danger|..."` (optionally combined with `surface=`) so the component picks the right token-based foreground/background pair (auto-switches in dark mode). Hardcoded Tailwind colors bypass theming.
- **Don't add `aria-label` to a button with visible text.** Screen readers read the visible label; the `aria-label` overrides it and creates a contradiction. Only set `aria-label` on icon-only buttons.

## Design Tokens

The button derives its color palette from `intent` × `surface` via `Pushery\WireKit\VariantResolver` — see [theming.md](/theming) for the mapping. The chrome below is shared across every combination:

| Token | Used for |
| --- | --- |
| `--font-wk-sans` | Button font family |
| `--font-wk-body-weight` | Font weight |
| `--font-wk-letter-spacing` | Letter spacing |
| `--font-wk-line-height` | Line height |
| `--text-wk-sm` / `--text-wk-md` / `--text-wk-lg` | Font size per `size` prop |
| `--size-wk-sm` / `--size-wk-md` / `--size-wk-lg` | Button height per `size` prop |
| `--padding-wk-x-sm` / `--padding-wk-x-md` / `--padding-wk-x-lg` | Horizontal padding per `size` |
| `--radius-wk-sm` / `--radius-wk-md` / `--radius-wk-lg` | Border radius per `size` |
| `--border-wk-width` | Border width |
| `--color-wk-ring` / `--color-wk-ring-offset` | Focus ring color + offset |
| `--ring-wk-width` / `--ring-wk-offset` | Focus ring geometry |
| `--opacity-wk-disabled` | Disabled visual weight |
| `--transition-wk-duration` / `--transition-wk-easing` | Hover / focus transition |

## Usage & Conventions

> **Prop conventions** — this component uses one or more of the shared semantic prop names (`intent` / `variant` / `tone` / `surface`). See [Prop naming conventions](/extending/prop-naming-conventions) for the canonical vocabulary, alias matrix, and decision tree.

## Further Reading

- [MDN: `<button>` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button)
- [WAI-ARIA Button Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/button/)
- [MDN: `type` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#type) — why `type="button"` matters inside `<form>`
