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.
Basic Usage
Sizes
As Link
Renders as <a> instead of <button>.
Loading State
<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:
<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:
Click the button.
Icons
With WireKit Icon Component
Use the iconLeft or iconRight slots together with the Icon component:
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:
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:
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.
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.
The same wrapper joins an input with a trailing button — an attached search field, a newsletter signup, or a copy-link row:
Width & Layout
Buttons auto-size to their content by default. Use Tailwind classes for full-width or fixed-width buttons:
{{-- 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).
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 withtype="button"by default, preventing accidental form submission. Override withtype="submit"when needed. -
Disabled state — The
disabledprop sets the nativedisabledattribute. The component appliesopacity-[var(--opacity-wk-disabled)]andpointer-events-noneso the button is visually muted and unreachable by pointer. -
Focus ring — Uses
focus-visible(notfocus) so the ring only appears for keyboard navigation, never for mouse clicks. -
Loading state — When
loadingis set, the button is automatically disabled during Livewire requests viawire:loading.attr="disabled", preventing double submissions. The spinner SVG includesaria-hidden="true". -
Icon-only buttons — When a button has no visible text label (icon only), add an
aria-labelattribute:<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
hrefandtarget="_blank"are used together, the component automatically injectsrel="noopener noreferrer"(prevents tabnabbing) and a screen-reader hint "(opens in new tab)". -
Keyboard —
EnterandSpaceactivate 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 issubmit— clicking the button will submit the surrounding form even if you wired awire:clickhandler. The component already injectstype="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. Useintent="primary|neutral|danger|..."(optionally combined withsurface=) 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-labelto a button with visible text. Screen readers read the visible label; thearia-labeloverrides it and creates a contradiction. Only setaria-labelon icon-only buttons.
Design Tokens
The button derives its color palette from intent × surface via Pushery\WireKit\VariantResolver — see theming.md 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 for the canonical vocabulary, alias matrix, and decision tree.
Further Reading
- MDN:
<button>element - WAI-ARIA Button Pattern
- MDN:
typeattribute — whytype="button"matters inside<form>