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 with accent-color and ::before overlays.
  • 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) or aria-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.

checkbox.html
<!-- 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.

Keyboard interactions for the custom checkbox
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.

ARIA attributes and where they live
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 preventDefault so 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-label the control is announced as an anonymous checkbox.