Skip to main content
Copy for LLM

Toggle

A switch control for binary on/off settings. Toggles are visually distinct from checkboxes and communicate an immediate state change (like flipping a lightswitch) rather than a form selection to be submitted later.

Basic Usage

Simple toggle

Sizes

Small, medium, large

With Hint

Toggle with helper text

Occasional product updates, no more than once a month.

States

Checked and disabled

The State in Words and Colors

on-label and off-label write the state beside the switch. The word follows the click at once, before anything is saved, because it is drawn from the checkbox itself. A screen reader does not read it: the switch announces its own state. The words sit after the switch and its label, or before the switch with state-position="start", and they keep the width of the longer one, so nothing beside them moves when the switch flips.

on-intent and off-intent color the track for each state: accent, success, danger or neutral. On is accent and off is neutral unless you say otherwise. neutral is the plain off state, so an on-intent="neutral" switch tells its states apart by the knob alone.

Green on, red off, with the state in words

Livewire Integration

The toggle is a native <input type="checkbox"> — wire:model works out of the box:

<x-wirekit::toggle
    name="notify"
    label="Notify me"
    wire:model.live="notifyEnabled"
/>

Bind a bool property, or a model attribute with a boolean cast. A number, such as a tinyint column without the cast, is taken for the box's value rather than its state, so the toggle shows off whatever the property holds and does not follow a value the server sets. In debug mode the component logs a warning for a binding to a number.

Optimistic UI

Pass the name of the Livewire method the toggle should call and it shows the new state immediately, then confirms or undoes it when the server answers:

<x-wirekit::toggle
    name="notify"
    label="Notify me"
    :checked="$notifyEnabled"
    optimistic="toggleNotify"
/>

Try it

The demo below runs the real path: the flip shows immediately, the outline says the value is provisional, and the server's answer either confirms it silently or takes it back.

Optimistic toggle — accept, reject, and a slow answer

The <livewire:demos.…> wrapper above exists only on this site — it supplies the three demo methods so the page can show a real round trip. The block under it is what you write.

It is a method name rather than a boolean because the component cannot know the action otherwise — server actions reach a WireKit component through the attribute bag, so the component never sees your wire:click.

optimistic replaces wire:model.live here rather than joining it. The method you name is what changes the value on the server, and the toggle shows the change while that call is in flight. Pass the current value with :checked so the toggle knows where it started; a plain wire:model (without .live) alongside it is still fine, since it only submits with the surrounding form.

Load wirekit-optimistic.js alongside whichever bundle you already use — below it, in your layout:

@wirekitScripts
<script src="{{ asset('vendor/wirekit/wirekit-optimistic.js') }}" defer></script>

It is a separate file on purpose: loading it is how you opt into the announcement behavior described below, so applications that do not use it pay nothing for it. Without it the toggle keeps working exactly as it always has — the prop simply has nothing to mount.

What a screen reader hears

The flip announces once, hedged — "Saving" — so the state is audible as provisional. Confirmation is silent: what was announced is what happened. Only a deviation speaks a second time, which is what makes the undo recognizable as an undo rather than as another confirmation.

Where the toggle already shows a validation error, the undo stays silent and leaves the field's own message to speak. "Email is required" tells you what to do; "could not save" does not.

An aborted request announces nothing at all — nothing was refused, so there is nothing to take back.

Focus stays exactly where you put it. An undo arrives on the server's schedule, and moving focus then would take you out of your place for a reason you could not predict.

A second flip while one is in flight is refused rather than queued. Queued, the final state would depend on the order the responses came back in — network timing — which is both wrong and impossible to test.

Scope. This covers mutations: changing something and showing it right away. Sorting, filtering, pagination and form submission are query round trips — nobody can show a result nobody knows yet — and are deliberately not covered.

Design Decisions

  • Why role="switch" instead of role="checkbox"? A switch represents an immediate state change (the setting applies right away). A checkbox represents a selection in a form (applied on submit). This distinction matters for screen reader users.
  • Why a native <input> instead of Alpine? Native inputs are zero-JS, fully compatible with Livewire's wire:model, and inherit form submission behavior for free.
Error state

Turn this on to receive the digest you subscribed to.

Props

Prop Type Default Description
label string|null null Text label displayed next to the toggle
id string|null derived from name The switch's id. A <label for> of yours with this id names the field, and then no fallback name stands over it
hideLabel bool false Keep the label as the control's accessible name but take it off the screen — for a toggle in a table column or toolbar whose surrounding chrome already names it. Same prop as input / select / textarea / combobox / checkbox
hint string|null null Helper text rendered below
help string|null null An explanation of the field, shown in a tooltip from a question mark beside the label and read as the field's description. Needs a label to stand beside. See Help beside the label
error string|null null Error message (also triggers aria-invalid)
groupError bool true Take the surrounding field.set's group error, or decline it with :group-error="false". A control that belongs to the group but neither causes the rejection nor can resolve it is then neither marked aria-invalid by the group nor described by its message; its own error, its own hint and the set's hint are unaffected
size string 'md' sm, md, lg
onLabel string|null null The word shown beside the switch while it is on. Hidden from assistive technology, which hears the switch's own state. See The State in Words and Colors
offLabel string|null null The word shown while it is off
statePosition string 'end' Where the words sit: end after the switch and its label, start before the switch
onIntent string 'accent' The track's color while on: accent, success, danger, neutral
offIntent string 'neutral' The track's color while off: neutral, accent, success, danger
scope string|null null Scoped personalization name
optimistic string|null null Livewire method to call, showing the new state before the server confirms it. See Optimistic UI.
optimisticArgs array [] Extra arguments appended to the optimistic action call, after the new value — the row this control belongs to.
announceError bool|null null Render the error message in an aria-live="polite" region so a validation error that appears after render reaches a screen reader. Precedence: this prop, then the parent <x-wirekit::form :announce-errors>, then config('wirekit.a11y.announce_error'). Set false when the page runs its own live region.

All other attributes (name, id, checked, disabled, wire:model, etc.) pass through to the underlying <input>.

Slots

Slot Description
labelAfter Markup after the label and its help question mark, in the same row and outside both, such as a badge that says a setting is not decided yet. See A Mark After the Label.

Accessibility

  • The underlying <input> has role="switch", which tells assistive technology this is an on/off control (not a standard checkbox).
  • The visual track and knob are purely decorative (aria-hidden="true"). Screen readers announce the native input's checked state.
  • The words from on-label and off-label are aria-hidden for the same reason: the switch already says whether it is on, and reading the word as well would say it twice.
  • The accent, success and danger tracks keep at least 3:1 against the page and against the knob, in the default palette, light and dark.
  • Focus ring appears on the track via peer-focus-visible:* utilities — keyboard users always see which toggle is focused.
  • aria-invalid, aria-describedby are set automatically when error or hint are present.
  • Inside <x-wirekit::field.set>, the toggle also points at the group's hint, or at the group's error while it carries aria-invalid.

Keyboard Interaction

Key Action
Tab Move focus to the switch
Space Flip the switch

The control is a native <input type="checkbox"> carrying role="switch", so it inherits the browser's own keyboard behavior: Space toggles it. Enter does not — inside a form a browser treats Enter on a checkbox as a submit, and the toggle binds no key handler that would change that. Enter is a button's key, and this control is not a button.

Pitfalls

  • Don't use a toggle for an action that requires a confirmation step. Toggles should commit instantly; pair them with reversible state. For destructive actions use a button + <x-wirekit::alert-dialog>.
  • Don't bind wire:model.live if the action is expensive. Each flick of the toggle round-trips. Debounce, or commit on a save button.

Design Tokens

Token Purpose
--color-wk-accent Track background when checked
--color-wk-success, --color-wk-danger Track background and border for the success and danger intents
--color-wk-border-strong Track background and border when off, and when on with on-intent="neutral". It holds 3:1 against the page and keeps the knob at 3:1 against the track
--color-wk-bg-elevated Knob background
--color-wk-ring Focus ring color
--shadow-wk-sm Knob shadow
--transition-wk-duration Knob slide + color animation
--opacity-wk-disabled Dimmed state when disabled

Customization

Override defaults in config/wirekit.php:

'components' => [
    'toggle' => ['size' => 'lg'],
],

Updated in WireKit v2.66.0 (2026-10-13)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store