Thanks to visit codestin.com
Credit goes to docs.wirekit.app

Skip to main content
WireKit
Copy for LLM

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.

Sandbox

Basic Usage

Common shapes

Sizes

Sizes
A button that is really a 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 to simulate a Livewire request

Click the button.

Icons

With WireKit Icon Component

Use the iconLeft or iconRight slots together with the Icon component:

Buttons with icon aliases

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:

Single button with raw SVG

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:

Icon component and raw SVG 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.

Primary intent across surfaces
Danger intent across surfaces
All intents with filled surface

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.

Joined button group
Vertical button group

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

Input with attached button

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-compact, md, lg, xl).

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.

A label somebody else typed

The default keeps a label on one line, and for a label you wrote that is right — Save, Send test, Delete forever all belong on one row, and one that wrapped mid-word would be the defect.

It is the wrong answer for a name a user typed. A template name, a channel label, a repository login are as long as somebody made them, and a row of buttons carrying them runs off the edge of its card rather than taking a second line. wrap-label hands that decision to the caller:

A label the user typed

The second button is the same label with the spinner running, and it is there on purpose: the reason the default clamps to one line is that a wrapping label used to push the spinner above the text. wrap-label holds the flex line together, so the label takes its second row and the spinner stays beside it.

The third is a name with no space or hyphen in it, which is the shape a template name, a channel or a repository usually has. It breaks inside the word, and only where nothing else fits: a label that does have a space still breaks at the space.

Reaching for class="whitespace-normal" from outside does not reliably work, and it is worth knowing why: two Tailwind utilities for the same property are decided by their order in the generated stylesheet, not by their order in your attribute. That is what this prop exists to settle.

Beside a Field on a Touch Screen

On a touch screen, form fields lift their text to 16px, because iOS zooms into a field with smaller text the moment it is focused. Buttons keep their size, which is right for a button on its own. A small button in one row with a small select or input then reads in a different type size, so give that button the same floor:

<x-wirekit::select size="sm" name="locale" :options="$locales" />
<x-wirekit::button size="sm" class="pointer-coarse:text-[length:var(--text-wk-touch-floor)]">Sign in</x-wirekit::button>

--text-wk-touch-floor is the value the fields read, so the two stay equal when a theme changes it. The class applies only on a touch screen; with a mouse the button keeps its size.

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. outlined is accepted as an alias of outline
size string 'md' xs, sm, md-compact, md, lg, xl. md-compact is the 2.25rem tier between sm and md, for dense toolbars — see Input for the worked example
type string 'button' HTML button type
wrapLabel bool false Let the label take a second line. The default keeps it on one, which is right for a label you wrote — a button that wraps mid-word is the defect there. Turn it on for a label a user typed: a template name, a channel label, an org login are as long as somebody made them, and a row of those runs off the edge of a card instead of wrapping. A name with no space in it breaks inside the word where nothing else fits. The loading spinner stays beside the text either way
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. Setting it turns the busy state on, so loading is not needed alongside it.
disableOnLoading bool true Whether the busy state disables the control or merely announces itself. disabled blurs the element the moment it applies, so the control the reader just activated loses focus for the whole in-flight window — WCAG 2.4.3, on every request. false emits aria-busy instead: the same announcement, still focusable, still in the tab order. The trade is yours — a control that stays enabled can be pressed twice.
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 — On a <button> the disabled prop sets the native disabled attribute. A link cannot be disabled natively — disabled is not a valid attribute on <a> — so on the href flavor the component withholds the href instead and marks the element role="link" aria-disabled="true": it drops out of the tab order, cannot be followed, and is still announced as the disabled link it is. Both flavors get opacity-[var(--opacity-wk-disabled)] and pointer-events-none, so the control 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", so the disable is what an assistive reader hears — which is also why it is the default.

  • Loading state without losing focusdisabled blurs the control the instant it applies. The element a reader just activated therefore stops being focused for the whole request and focus falls to the document body, which is a WCAG 2.4.3 failure on every request rather than an edge case. Set :disable-on-loading="false" and the busy state becomes aria-busy instead: announced the same way, still focusable, still in the tab order. Weigh it against the double-submit the disable prevents; both are real, and only you know which matters on that screen.

  • Icon-only buttons — When a button has no visible text label (icon only), add an aria-label attribute:

    <x-wirekit::button intent="neutral" surface="ghost" aria-label="Close">
        <x-slot:iconLeft><x-wirekit::icon name="close" size="sm" /></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)".

  • KeyboardEnter 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 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-compact / --size-wk-md / --size-wk-lg Button height per size prop. xs and xl have no token of their own — they are calc() off --size-wk-sm and --size-wk-lg, so retuning those two moves all four rungs together.
--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

Updated in WireKit v2.51.0 (2026-09-14)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store