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
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.
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.
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. 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.
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.
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>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. - The words from
on-labelandoff-labelarearia-hiddenfor the same reason: the switch already says whether it is on, and reading the word as well would say it twice. - The
accent,successanddangertracks 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-describedbyare set automatically whenerrororhintare present.- Inside
<x-wirekit::, the toggle also points at the group's hint, or at the group's error while it carriesfield. set> 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.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-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/:
'components' => [
'toggle' => ['size' => 'lg'],
],