Skip to main content
WireKit
Copy for LLM

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

Simple checkbox

States

Unchecked, checked, indeterminate, disabled

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.

Checkbox sizes

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.

Selectable cards

With Hint

Hint text

Monthly product updates, unsubscribe anytime.

With Error

Error state

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:

Select-all parent with indeterminate state

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's x-model, drop the value="..." attribute and bind to a boolean state (one key per checkbox), not to an array. With value present, Alpine expects x-model to 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 the x-model / value semantics 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:

Checkbox with links inside the label slot

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.

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.

Consent checkbox with in-page modal links

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 an x-data scope. Without it, x-on:click on the inline buttons would be silently ignored. The x-data can 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.

Optimistic checkbox — accepted, refused, and a slow answer

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-invalid and aria-describedby are set automatically when error or hint are 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's label prop (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

Was this page helpful?

Thanks — that helps.

Voting requires cookies or local storage. What we store