Checkbox
A form control for binary choices or multi-select lists. Supports checked, unchecked, and indeterminate states with full keyboard and screen-reader support.
Basic Usage
States
Sizes
Three sizes scale the box and its checkmark together — sm, md (default), and lg. They line up with the matching size on Radio and Toggle so a form mixing the controls stays visually consistent.
Card Variant
variant="card" turns the whole control into a bordered, fully-clickable card that highlights when checked — the canonical "selectable option" pattern for feature toggles and multi-select grids. The card reacts to its own input via CSS :has(), so it needs no JavaScript.
With Hint
With Error
This field is required.
Errors from Laravel's validation bag are shown automatically when name matches a field. In a Livewire form, the error disappears once the field passes validation.
Indeterminate State
The indeterminate state is useful for "parent" checkboxes when only some of their children are checked — e.g. a "Select all" control. The demo below uses Alpine.js to keep the parent in sync with its children:
The indeterminate state is set via the DOM property input.indeterminate = true — there is no
HTML attribute for it, so the server cannot render it and something has to apply it after every
render.
WireKit emits data-wk-indeterminate="true|false" on the input in both states and watches
it. That is deliberately not x-init, which runs once: the third state almost always arrives
after the first render — a Livewire round trip morphs the element, the attribute text changes,
Alpine does not re-initialize, and the property keeps its old value. The box then reads as
"none selected" while something is selected, which is exactly the state it exists to show. The
attribute is also written when the prop is false, so returning to a determinate state has
something to undo it.
It is not x-effect either. An effect over a value the server rendered re-runs exactly as
often as x-init does — once — so x-effect is right where the value is reactive Alpine state
(as in data-table) and wrong here.
x-bind:indeterminate (above) is the other half and stays as it is: it reacts to live Alpine
state, which is a different source from the server-rendered prop.
Verifying it from a consuming app
Grep the installed package for data-wk-indeterminate in
resources/views/components/checkbox.blade.php, not for x-effect — the mechanism
deliberately avoids the latter, so its absence says nothing about the capability.
Important: When binding an array-style
name(e.g.name="stack[]") with Alpine'sx-model, drop thevalue="..."attribute and bind to a boolean state (one key per checkbox), not to an array. Withvaluepresent, Alpine expectsx-modelto target an array and will silently mis-render the checked state. WireKit auto-generates unique DOM ids for array-style names so<label for>keeps working, but thex-model/valuesemantics remain your responsibility.
With Rich Label (Slot)
For labels that contain links or formatting — common in GDPR/privacy consent checkboxes — use the default slot instead of the label prop:
The slot content is rendered as raw HTML, so links, <strong>, <em>, and other inline elements work. When both label and slot content are provided, the slot takes precedence.
Open Links in a Modal
A common pattern for newsletter sign-ups: link the privacy policy and terms to static pages that open in a WireKit modal instead of navigating away.
The <button type="button"> prevents the checkbox from toggling when clicking a link (the .stop modifier makes sure the click doesn't bubble up to the label and retrigger a toggle). The modal opens by dispatching wirekit-modal-show directly on window — this guarantees the listener is hit regardless of DOM position.
Why the
<div x-data>wrapper? Alpine directives (x-on,$dispatch,$refs, …) only work inside anx-datascope. Without it,x-on:clickon the inline buttons would be silently ignored. Thex-datacan be empty — its job here is simply to activate the Alpine component tree.
Livewire Integration
Bind a checkbox to a boolean Livewire property via wire:model.live for single-toggle controls. For multi-select groups, share a name="…[]" across the group with a unique value per checkbox and bind every input to the same array property — the framework collects checked values into the array.
<x-wirekit::checkbox name="terms" label="Accept terms" wire:model.live="acceptedTerms" />
{{-- Array binding (multi-select) --}}
<x-wirekit::checkbox name="tags[]" value="php" label="PHP" wire:model.live="selectedTags" />
<x-wirekit::checkbox name="tags[]" value="js" label="JavaScript" wire:model.live="selectedTags" />
Optimistic UI
Pass the name of the Livewire method this component should call and the change appears immediately, then confirms or undoes itself when the server answers:
<x-wirekit::checkbox
name="notify"
label="Notify me"
:checked="$notify"
optimistic="toggleNotify"
/>
Load wirekit-optimistic.js alongside whichever bundle you already use — it is a separate file so applications that do not use it pay nothing for it:
@wirekitScripts
<script src="{{ asset('vendor/wirekit/wirekit-optimistic.js') }}"></script>
Try it
The demo below runs the real path: the change shows immediately, the outline says it 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 demo
methods so the page can show a real round trip. The block under it is what you write.
The browser has already ticked the box by the time any handler runs, so what this adds is the confirmation, the undo, and the announcement. Pass the current value with :checked so the component knows where it started; optimistic replaces wire:model.live rather than joining it.
What a screen reader hears is the same for every component that supports this, and it is written out in full — one hedged announcement at the change, silence on confirmation, one more only if the server refuses, and focus that never moves — in Optimistic UI — the announcement contract. That page is also where the boundary is stated: this covers mutations, not sorting, filtering or pagination.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
label |
string|null | null |
Text next to the checkbox |
hideLabel |
bool | false |
Render the label sr-only (kept as the control's accessible name) — for a checkbox in a table column whose header already names it. Same prop as input / select / textarea / combobox |
hint |
string|null | null |
Helper text below |
error |
string|null | null |
Error message (also triggers aria-invalid) |
indeterminate |
bool | false |
Show dash instead of check (requires Alpine) |
size |
string | 'md' |
Box size — sm, md, or lg |
variant |
string | 'default' |
default (inline) or card (the whole bordered card is the selectable target) |
announceError |
bool | true (from config('wirekit.a11y.announce_error')) |
Announce the error in an aria-live="polite" region so a validation error that appears after render reaches a screen reader. Set false when the page runs its own live region |
scope |
string|null | null |
Scoped personalization name |
Slots
| Slot | Description |
|---|---|
| Default | Rich HTML label content (links, formatting). Takes precedence over the label prop. |
All other attributes pass through to <input>, including name, id, checked, disabled, value, wire:model.
Accessibility
- Uses a native
<input type="checkbox">— all keyboard and screen-reader behavior is inherited from the browser. - The visual box and checkmark are purely decorative (
aria-hidden="true"). - Focus ring appears on the box when the hidden input receives keyboard focus.
aria-invalidandaria-describedbyare set automatically whenerrororhintare present.
Keyboard Interaction
| Key | Action |
|---|---|
Tab |
Move focus to the checkbox |
Space |
Toggle the checked state |
Pitfalls
- Don't omit
<x-wirekit::label>association. Labeling via the component'slabelprop (or the<x-slot:label>slot) is what makes the entire label clickable AND announces the binding to screen readers. - Don't wrap a checkbox in a
<button>. The component already renders an interactive<input type="checkbox">— wrapping it in another interactive element confuses focus order.
Design Tokens
| Token | Purpose |
|---|---|
--color-wk-accent |
Fill color when checked |
--color-wk-accent-fg |
Checkmark + dash color |
--color-wk-bg-input |
Unchecked box background |
--color-wk-border-strong / --color-wk-border-error |
Box border |
--radius-wk-sm |
Box corner radius |
--color-wk-ring |
Focus ring |
Further Reading
- MDN:
<input type="checkbox"> - WAI-ARIA Checkbox Pattern
- MDN:
aria-checked— when building custom checkboxes - MDN:
:checkedpseudo-class