Custom checkbox
A checkbox is a control that holds a binary on/off state the user can toggle. The web
already ships one — <input type="checkbox"> — and it is
keyboard-operable, form-submittable, and screen-reader-ready with no JavaScript at all.
Reach for the native control first, every time. This page exists for
the rare case where you genuinely cannot use or style the native checkbox and must
rebuild it from a generic element with ARIA.
The custom widget below follows the WAI-ARIA
Authoring Practices checkbox pattern: a role="checkbox" element with
tabindex="0", an aria-checked state, and an accessible name.
The markup is authored statically, and checkbox.js enhances it — but even
with scripting off, the control still announces its state to assistive technology.
Live demo
First, the control you should almost always use: a styled native checkbox. It already toggles on Space, submits with a form, and exposes its state for free.
Only when the native control truly cannot be styled, rebuild it. Focus the box below
with Tab and press Space to toggle it. The state lives in
aria-checked, and the tick is drawn by CSS so the on/off difference is a
shape, not just a colour.
Email me about new patterns (custom role="checkbox")
When to use
Prefer the native control first
- For a single on/off choice, use
<input type="checkbox">with a real<label>. It is keyboard-, form-, and screen-reader-complete for free, and modern CSS can restyle it heavily withaccent-colorand::beforeoverlays. - Build this ARIA checkbox only when a design system genuinely forbids the native control or it cannot be styled to spec — and then accept that you now own every behaviour it gave you.
- Whichever you choose, the control must keep a real, programmatic name via a
wrapping
<label>(native) oraria-labelledby/aria-label(custom). - Never convey checked-ness with colour alone — pair the fill with a drawn tick or other non-colour cue.
Markup
Author the control statically with its role, focusability, state, and name already in
place. checkbox.js only adds the toggling behaviour; it does not invent the
ARIA wiring.
<!-- Prefer this: a native checkbox, styled if you like -->
<label>
<input type="checkbox">
Email me about new patterns
</label>
<!-- Only when native truly cannot be styled: -->
<span class="ewa-check" role="checkbox" tabindex="0"
aria-checked="false" aria-labelledby="ck-l"></span>
<span id="ck-l">Email me about new patterns</span>
Keyboard interactions
The custom control must mirror the native checkbox exactly: it is reachable in the tab order and toggles on Space.
| Key | Action |
|---|---|
| Tab | Moves focus to the checkbox (it has tabindex="0"). |
| Space | Toggles the control between checked and unchecked; the page does not scroll
(preventDefault). |
ARIA roles, states, and properties
A generic element only becomes a checkbox to assistive technology once it carries the role, a focusable tab stop, a state, and an accessible name.
| Attribute | On | Purpose |
|---|---|---|
role="checkbox" |
The control | Identifies the generic element as a checkbox to assistive technology. |
tabindex="0" |
The control | Puts the control in the tab order so keyboard users can reach it. |
aria-checked |
The control | true when checked, false when not. (Use
mixed only for a tri-state parent checkbox.) |
aria-labelledby |
The control | Names the checkbox from the visible label element’s id. Use
aria-label instead only when there is no visible text. |
Common mistakes
Pitfalls to avoid
- Rebuilding a checkbox you didn’t need to. A real
<input type="checkbox">is keyboard-, form-, and screen-reader-ready for free. Only build a custom one when you truly cannot style the native control — and then replicate all of its behaviour. - Forgetting the tab stop. Without
tabindex="0"the custom control can never receive keyboard focus, so it is unusable by keyboard. - Toggling on click but not Space. A pointer-only toggle
locks out keyboard and many switch users; wire the Space key and call
preventDefaultso the page doesn’t scroll. - Showing state with colour alone. The checked fill must be paired with a drawn tick (or similar shape) and remap to system colours under forced colours.
- Leaving it unnamed. Without
aria-labelledby/aria-labelthe control is announced as an anonymous checkbox.