Accessibility pattern · Form inputs

Checkbox

Each box is a real input type=checkbox with appearance: none, so the tick, the mixed bar and the focus ring are drawn in CSS while the browser keeps the role, the state and Space. The label covers the whole row, so the target is the full width and at least 44 pixels high, and each description is joined with aria-describedby.

WCAG criteria
7
Keyboard rules
4
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Before you pay

2 items · ₹2,140

Handmade paper and a card, ₹49 per item.

The courier takes a photo as proof of delivery.

Not available for orders over ₹2,000.

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
TabMoves to the next checkbox; Pay on delivery is disabled and skipped.
ShiftTabMoves to the previous checkbox.
SpaceTicks or clears the focused checkbox. On Gift wrap, ticks or clears both items under it.
EnterOn Place order, sends the form. If the terms are not agreed, focus moves to that checkbox and its error.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Focus reaches Gift wrap with one item tickedGift wrap, checkbox, partly checked. Handmade paper and a card, ₹49 per item.
Space ticks itChecked (both items under it are ticked too)
Tab reaches an item under itGift wrap, group. Block-print kurta, checkbox, checked
Focus reaches the termsI agree to the terms of sale, checkbox, not checked, required. Returns within 30 days. Handmade pieces can vary slightly.
Place order is pressed without agreeingI agree to the terms of sale, checkbox, not checked, invalid entry, required. Returns within 30 days… Agree to the terms of sale to place your order
The order is placedOrder placed. Your receipt is on its way.

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<form class="ap-checkbox" data-ap-checkbox novalidate aria-labelledby="cb-form-name">
  <div class="ap-checkbox__top">
    <h3 class="ap-checkbox__name" id="cb-form-name">Before you pay</h3>
    <p class="ap-checkbox__sub">2 items · ₹2,140</p>
  </div>

  <div class="ap-checkbox__list">
    <div class="ap-checkbox__row">
      <input class="ap-checkbox__box" type="checkbox" id="cb-wrap" name="wrap" aria-controls="cb-wrap-kurta cb-wrap-diya" aria-describedby="cb-wrap-desc" />
      <div class="ap-checkbox__text">
        <label class="ap-checkbox__label" id="cb-wrap-label" for="cb-wrap">Gift wrap</label>
        <p class="ap-checkbox__desc" id="cb-wrap-desc">Handmade paper and a card, ₹49 per item.</p>
      </div>
    </div>
    <div class="ap-checkbox__children" role="group" aria-labelledby="cb-wrap-label">
      <div class="ap-checkbox__row ap-checkbox__row--child">
        <input class="ap-checkbox__box" type="checkbox" id="cb-wrap-kurta" name="wrap-item" value="kurta" checked />
        <div class="ap-checkbox__text">
          <label class="ap-checkbox__label" for="cb-wrap-kurta">Block-print kurta</label>
        </div>
      </div>
      <div class="ap-checkbox__row ap-checkbox__row--child">
        <input class="ap-checkbox__box" type="checkbox" id="cb-wrap-diya" name="wrap-item" value="diya" />
        <div class="ap-checkbox__text">
          <label class="ap-checkbox__label" for="cb-wrap-diya">Brass diya set</label>
        </div>
      </div>
    </div>

    <div class="ap-checkbox__row">
      <input class="ap-checkbox__box" type="checkbox" id="cb-door" name="door" aria-describedby="cb-door-desc" />
      <div class="ap-checkbox__text">
        <label class="ap-checkbox__label" for="cb-door">Leave at the door if nobody answers</label>
        <p class="ap-checkbox__desc" id="cb-door-desc">The courier takes a photo as proof of delivery.</p>
      </div>
    </div>

    <div class="ap-checkbox__row">
      <input class="ap-checkbox__box" type="checkbox" id="cb-cod" name="cod" disabled aria-describedby="cb-cod-desc" />
      <div class="ap-checkbox__text">
        <label class="ap-checkbox__label" for="cb-cod">Pay on delivery</label>
        <p class="ap-checkbox__desc" id="cb-cod-desc">Not available for orders over ₹2,000.</p>
      </div>
    </div>
  </div>

  <div class="ap-checkbox__consent">
    <div class="ap-checkbox__row">
      <input class="ap-checkbox__box" type="checkbox" id="cb-terms" name="terms" required aria-describedby="cb-terms-desc" />
      <div class="ap-checkbox__text">
        <label class="ap-checkbox__label" for="cb-terms">I agree to the terms of sale <span class="ap-checkbox__req" aria-hidden="true">Required</span></label>
        <p class="ap-checkbox__desc" id="cb-terms-desc">Returns within 30 days. Handmade pieces can vary slightly.</p>
      </div>
    </div>
    <p class="ap-error ap-checkbox__error" id="cb-terms-error" hidden><svg class="ap-checkbox__icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5"/><path d="M12 16.5h.01"/></svg><span>Agree to the terms of sale to place your order</span></p>
  </div>

  <div class="ap-checkbox__foot">
    <p class="ap-checkbox__status" role="status"></p>
    <button type="submit" class="ap-btn ap-btn--primary">Place order</button>
  </div>
</form>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    Every box has a label tied with for and id, each description is joined with aria-describedby, and the gift wrap items sit in a group named by their parent.

  • 1.4.1 Use of Color Level A

    Checked shows a tick, mixed shows a bar, disabled shows a dashed edge, and an error shows an icon and a message, so no state is color alone.

  • 1.4.11 Non-text Contrast Level AA

    The unchecked edge, the filled box and the focus ring all clear 3:1 against the card, in light and dark.

  • 2.5.8 Target Size (Minimum) Level AA

    The whole row is the label, so each target is the full width of the card and at least 44 pixels high, well above 24.

  • 3.3.1 Error Identification Level A

    Placing the order without agreeing marks the consent box aria-invalid and explains why in text under it.

  • 3.3.2 Labels or Instructions Level A

    Required is written beside the consent label, and each option's description says what it does before it is chosen.

  • 4.1.2 Name, Role, Value Level A

    Native checkboxes report checked, not checked, partly checked, required and unavailable without any ARIA.

Usage

When to use it

Use it

  • Options that can be chosen independently, in any combination, including none.
  • A single yes-or-no agreement that has to be given on purpose, like accepting terms.

Use something else

  • Choosing exactly one of several: use a radio group.
  • A setting that takes effect at once, with no form to send: a switch says that better.
  • Long lists where people pick a few: consider a multi-select or a filter list with search.

Common failures

How it usually goes wrong

  • A div drawn to look like a checkbox

    Without a real input it has no role, no state and no Space key unless all three are rebuilt. appearance: none restyles the native box and keeps all of that.

  • Only the tiny box is clickable

    A 16 pixel box is hard to hit with a tremor or on a phone. The label here covers the whole row.

  • The description inside the label

    Putting the help text inside the label makes it part of the name, so it is read every time and becomes harder to match by voice. It sits beside the label and is joined with aria-describedby.

  • A tick that fades into the background

    A pale tick on a pale fill fails 3:1 and vanishes in sunlight. The box here fills with the accent and the tick is white on it.

  • Mixed drawn like checked

    If partly checked looks the same as checked, people cannot tell that some items are left out. It is drawn as a bar, and announced as partly checked.

  • Consent ticked in advance

    A box already ticked is not consent, and an error that only turns the box red is missed. It starts empty, and the error says what to do.

Notes

Building it

  • There is no HTML attribute for the mixed state: the script sets the indeterminate property from the items each time one changes, and on start-up.
  • aria-controls on Gift wrap names the items it changes, as in the WAI-ARIA mixed checkbox example.
  • The row's focus ring is drawn with :has(:focus-visible), so it surrounds the whole target rather than the 20 pixel box.
  • In Windows high contrast the fills are dropped; the forced-colors rules paint checked boxes with the system highlight so the tick stays visible.
  • Leave disabled options out of the Tab order, as the browser does, and say why they are unavailable in a description people can read.

Sources: WAI-ARIA Authoring Practices: Checkbox · WAI-ARIA Authoring Practices: Mixed-state checkbox example

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction