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
Sizes
With Hint
Occasional product updates, no more than once a month.
States
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"
/>
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.
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') }}"></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 ofrole="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'swire:model, and inherit form submission behavior for free.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label |
string|null | null |
Text label displayed next to the toggle |
hint |
string|null | null |
Helper text rendered below |
error |
string|null | null |
Error message (also triggers aria-invalid) |
size |
string | 'md' |
sm, md, lg |
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 a live region. Falls back to the parent form, then to config. |
All other attributes (name, id, checked, disabled, wire:model, etc.) pass through to the underlying <input>.
Accessibility
- The underlying
<input>hasrole="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. - Focus ring appears on the track via
peer-focus-visible:*utilities — keyboard users always see which toggle is focused. aria-invalid,aria-describedbyare set automatically whenerrororhintare present.
Keyboard Interaction
| Key | Action |
|---|---|
Tab |
Move focus to the button |
Enter / Space |
Activate the 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.liveif 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-bg-muted |
Track background when off |
--color-wk-bg-elevated |
Knob background |
--color-wk-border-strong |
Track border when off |
--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'],
],