Checkbox

User Experience (UX)

How a checkbox behaves for every person, input and setting, so it's accessible, predictable, clear in every state and adaptive.

Now let's look at how a checkbox behaves for every person, input and setting. A good checkbox is accessible, predictable, designed in every state and adaptive.

Accessible

Use the native element

A native <input type="checkbox"> gives you focus, Space to toggle it, the checkbox role with its checked and mixed states, a label that toggles it, form submission and <fieldset disabled>, all for free. A <div> with role="checkbox" only gives you the role. Everything else is one more thing to rebuild by hand, and it's easy to miss one, like Space, which is why so many custom checkboxes only work with a mouse.

{/* Do: you get the keyboard, the label, the states and form submission for free */}
<label>
  <input type="checkbox" name="terms" />
  I agree to the terms
</label>

{/* Don't: there's no Space, label, form submission or disabled fieldset until you rebuild them */}
<div className="checkbox" role="checkbox" aria-checked={agreed} tabIndex={0} onClick={toggle}>
  I agree to the terms
</div>

The Checkbox styles the native box with appearance: none and draws its own border, fill and check, so it looks the same in every browser while it keeps everything the browser gives it.

Label every checkbox

The label is also the checkbox's accessible name. Screen readers read it along with the state, as in "I agree to the terms, checkbox, not checked", voice control users say it out loud ("click I agree to the terms"), and tests and agents use it to find the box.

About once a month. You can stop them at any time.

<Checkbox name="updates" aria-describedby="updates-hint">Email me product updates</Checkbox>
<p id="updates-hint">About once a month. You can stop them at any time.</p>
  • Every checkbox has a visible label right after its box (WCAG 1.3.1, 3.3.2). The Checkbox wraps its own <label> around the box and the text you give it, so the text names the box without an ID, and a click on the text toggles it, which makes for a much bigger target.
  • Keep links and buttons out of the label, like a link to the terms. Their text becomes part of the checkbox's name, and a press meant for the box can land on the link instead. Put the link in the hint, or right before the checkbox.
  • A hint, like how often you'll send those emails, goes in the checkbox's description, which screen readers read after its name and state. Keep it outside the Checkbox, so it doesn't become part of the name, and point aria-describedby at its id.
  • A checkbox that its context already names, like the one in each row of a table, is the one checkbox that doesn't need visible text. Give it an aria-label that says what it selects, like aria-label="Select Ada Lovelace", rather than one that repeats "Select" in every row.
  • If you name a checkbox with aria-label or aria-labelledby while it has visible text, make sure the name includes that text, ideally at the start (WCAG 2.5.3).

Contrast and size

  • The label needs a contrast of at least 4.5:1 with its background (WCAG 1.4.3). Disabled checkboxes are exempt, but they should still be readable.
  • The box's border needs 3:1 against the page, since an unchecked box is nothing but its border, and so does a checked box's fill, while its check or dash needs 3:1 against the fill (WCAG 1.4.11). The focus ring needs 3:1 as well.
  • The box sits in a hit area of at least 24 × 24px (WCAG 2.5.8). The label adds to that, since a press on it toggles the box too, but we don't count it, because a checkbox without visible text still needs a box that's easy to press. A smaller box meets WCAG through its spacing exception, as long as a 24px circle centred on it doesn't overlap another target, or another small target's circle. That's easy to break in a dense table, so the hit area is the safer bet.

Show focus

When a checkbox has focus, its box shows an outline in the same focus colour as every other component (WCAG 2.4.7). Like the Button's, it's at least 2px thick and drawn outside the border, and we recommend 2px for both. It goes around the box rather than the label, so it marks the thing Space will toggle, and it shows on :focus-visible, so it's there when people reach the box with the keyboard, and not after every click.

You'll also want to keep focused checkboxes clear of sticky headers and footers, which you can do with scroll-padding (WCAG 2.4.11, 2.4.12).

Predictable

Keyboard

KeyCheckbox
Tab, Shift+TabMoves focus to and from the box
SpaceChecks or unchecks it, and clears the indeterminate state, unless it's read-only
EnterNever toggles it

Space toggles a checkbox in every browser, which is what the APG asks for, and what keyboard users expect. Enter never does. Chrome, for example, submits the box's form instead, through its submit button, just as it would from a text input, which HTML calls implicit submission. So don't make Enter toggle the box, and don't rely on it to submit the form either, since that's up to the browser.

Say what checking it means

A checkbox says yes or no to one thing, so its label should say what yes means, as in "Email me product updates". People shouldn't have to work out what an unchecked box means, so try to keep labels positive. "Don't email me" makes people check a box to say no, and screen readers read it as "Don't email me, checkbox, checked", which takes a moment to untangle.

Checking a box also shouldn't do anything on its own. It's a choice that the form sends when people submit it, so they can change their mind until then. For a setting that takes effect the moment you flip it, like Wi-Fi, the Switch says so much better. And for a choice of one option out of a few, a Radio Group is the one to use, since checkboxes tell people they can check as many as they like.

Every state

Indeterminate

An indeterminate checkbox is neither checked nor unchecked, which is what a "Select all" box shows when only some of its options are checked. Screen readers announce it as "mixed", or "partially checked", and the box shows a dash in place of its check.

<Checkbox indeterminate>Select all</Checkbox>
  • Set it as the indeterminate property of the <input>. HTML has no attribute for it, so an indeterminate attribute in your markup does nothing, and the Checkbox sets the property for you, which is also what makes screen readers announce it as mixed.
  • It's a look, not a value. Under the dash, the box is still checked or unchecked, and its form sends whichever it is.
  • The browser clears it on the first click or Space, and checks or unchecks the box as usual, as you can try above. The Checkbox fires an update when it does, so a value you bind to it follows along.
  • Work it out from the options it stands for each time they change, rather than keeping it as state of its own, so it can never disagree with them. The Checkbox Group does that for its select-all box.

Disabled

A disabled checkbox can't be focused, toggled or submitted. disabled sets the native attribute, so the box leaves the tab order, a press on its label does nothing, screen readers announce it as dimmed or unavailable when they read through the page, and its form leaves it out when it's sent, even when it's checked. A <fieldset disabled> does all of that for every checkbox inside it, and the Checkbox looks disabled there too, since its look comes from :disabled.

We recommend disabling as little as you can, since a disabled checkbox can't explain why it's disabled, and keyboard users can't even reach it to find out. Explain why next to it, or leave the choice out until it applies.

Read-only

A read-only checkbox shows a choice that people can see but not change, like a setting that their organisation manages for them. Unlike a disabled one, it stays in the tab order, so people can reach it and hear its state, and its form still sends it. That makes it the one to use when people need to see the choice and the form needs to send it.

HTML gives you nothing to lean on here, since it ignores readonly on a checkbox, and :read-only matches every checkbox there is. So readonly on the Checkbox sets aria-readonly="true" on the box, which tells assistive technologies that it can't be changed, and cancels the click that would toggle it, whether it comes from the box, its label or Space. A cancelled click puts back both the checked and the indeterminate state, and fires no change, so nothing else on the page has to know.

Where the Checkbox renders on the server alone, a script cancels the click instead, and it's the same script for every read-only box on the page:

<Checkbox name="updates" defaultChecked readOnly>Email me updates</Checkbox>

We recommend saying why next to it, as for a disabled checkbox, like "Your organisation manages this setting". The dashed border tells people they can't change it, but not why, or who can.

Required and invalid

Most checkboxes are optional, since an unchecked box is an answer too. The exception is a box that people have to check to go on, like agreeing to the terms. Give it required, which screen readers announce, and when people submit without checking it, show an error message that says why it matters.

import { useRef, useState, type FormEvent } from "react";
import { flushSync } from "react-dom";

export function CreateAccount() {
  const [agreed, setAgreed] = useState(false);
  const [error, setError] = useState("");
  const [status, setStatus] = useState("");
  const terms = useRef<HTMLInputElement>(null);

  function update(checked: boolean) {
    setAgreed(checked);
    // Checking the box fixes the error, so it clears right away.
    if (checked) setError("");
  }

  async function createAccount(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (!agreed) {
      // Focus once the error is in the page, so screen readers read it with the checkbox.
      flushSync(() => {
        setError("Agree to the terms to create an account");
        setStatus("Couldn't create your account. Check the terms.");
      });
      terms.current?.focus();
      return;
    }
    await api.createAccount();
    setStatus("Your account is ready");
  }

  return (
    <form noValidate onSubmit={createAccount}>
      <Checkbox
        ref={terms}
        name="terms"
        required
        checked={agreed}
        aria-invalid={error ? true : undefined}
        aria-describedby={error ? "terms-error" : undefined}
        onChange={(event) => update(event.target.checked)}
      >
        I agree to the terms
      </Checkbox>
      {error && <p id="terms-error">{error}</p>}
      <Button type="submit" color="primary">Create account</Button>
      <p role="status">{status}</p>
    </form>
  );
}
  • Set aria-invalid="true" on the box while its error message shows, and point aria-describedby at the message. Screen readers then announce the box as invalid, and read the message after its name (WCAG 3.3.1). If it has a hint too, list both IDs, the hint's first.
  • Say why the box needs checking, as in "Agree to the terms to create an account", rather than "This field is required" (WCAG 3.3.3). A red border alone can't say any of that, and it means nothing to people who can't tell red apart (WCAG 1.4.1).
  • Check it when people submit, rather than when they move past the box, since leaving a box unchecked is a perfectly good answer until then. Once the error shows, clear it as soon as they check the box.
  • When a submit fails, move focus to the first invalid control, once the error is in the page, as on the Input. The status message says what happened even when the focus doesn't move, like when people pressed Enter with the button already focused.
  • Put novalidate on the form, so that the browser's own error bubbles don't compete with yours. Keep required on the box, though, which tells screen readers that it's required.

Adaptive

Pointer, touch and pen

  • Hover styles only apply to pointers that can actually hover (@media (hover: hover)), otherwise a tap would leave the border looking hovered.
  • A press anywhere in the box's 24 × 24px hit area, or on its label, toggles it, unless it's read-only, since both are part of the <label>. The box and its check don't stop the press, so it reaches the <input> underneath.
  • Fingers are less precise than a mouse, so we recommend spacing stacked checkboxes 8px or more apart. That way, a tap that lands between two of them doesn't toggle the wrong one.

More contrast

When someone asks for more contrast (prefers-contrast: more), as with Increase contrast on macOS and iOS, the box's border is drawn in the text colour at full strength, rather than at 55%. The label, the fill and the check already pass, so they stay as they are.

Forced colours

Windows contrast themes replace every colour with the user's own, and drop backgrounds and shadows. The box's border stays, since the system draws it in a colour of its own, and so does focus, which is drawn with outline. A checked or indeterminate box is filled with Highlight, with its check or dash in HighlightText, a read-only box keeps its dashed border, with its check in CanvasText, and a disabled one is marked with GrayText. The invalid border's second pixel is a shadow, which forced colours drop, so in these themes it's the error message, in text, that tells people something's wrong.

Colour scheme

The tokens use light-dark(), which follows color-scheme, so the Checkbox adapts to your light and dark themes on its own.

Text size and spacing

The box, the gap and the hit area are set in rem, so the box grows with its text instead of looking tiny next to it, at 200% text size (WCAG 1.4.4) and with wider line, letter and word spacing (WCAG 1.4.12). A label that's longer than its column wraps, with the box beside its first line, rather than being clipped.

Languages and direction

The Checkbox lays out its box and label in a grid, which follows the reading direction, so the box sits at the start of the line in right-to-left languages too, with no extra work on your part:

<div dir="rtl" lang="ar">
  <Checkbox defaultChecked>أرسل لي التحديثات عبر البريد الإلكتروني</Checkbox>
  <Checkbox>أوافق على الشروط</Checkbox>
</div>
  • The check mark stays as it is in right-to-left layouts, as it does in native checkboxes, since it doesn't point anywhere.
  • Remember to translate labels, hints and error messages along with the rest of the page, and to mark any that are in another language with lang, so that screen readers pronounce them correctly (WCAG 3.1.2).

UX Checklist

RuleLevelScopeRequirementBasisCheck
checkbox/native-elementMustComponentIt's a native <input type="checkbox">, never an element with role="checkbox"Open Components, beyond WCAG 2.1.1, 4.1.2 (A)Unit test
checkbox/label-togglesMustComponentIts <label> wraps the box and its text, so the text names it and a press on the text toggles it, unless it's read-onlyWCAG 1.3.1, 4.1.2 (A), HTMLUnit test
checkbox/visible-labelMustUsageEvery checkbox has visible text, unless its context names it, like a row in a table, in which case it has an aria-labelWCAG 1.3.1, 3.3.2, 4.1.2 (A)axe label
checkbox/label-in-nameMustUsageAn aria-label or aria-labelledby includes the visible text, ideally at the startWCAG 2.5.3 (A)axe label-content-name-mismatch
checkbox/no-interactive-labelShouldUsageThe label holds no links or buttons, which go in the hint or before the checkboxOpen ComponentsReview
checkbox/positive-labelShouldUsageThe label says what checking the box means, in the positive, like "Email me updates"Open ComponentsReview
checkbox/describedShouldUsageHints sit outside the label and are linked with aria-describedbyWCAG 1.3.1 (A)Screen reader
checkbox/decorative-indicatorMustComponentThe check and the dash are hidden from assistive technologies with aria-hidden, so the name is the label aloneWCAG 1.1.1, 4.1.2 (A)Unit test
checkbox/keyboardMustComponentTab reaches the box, Space checks or unchecks it unless it's read-only, and Enter never toggles itWCAG 2.1.1 (A), APGUnit test
checkbox/indeterminateMustComponentThe mixed state is set as the <input>'s indeterminate property, so it's announced as mixed, and it clears on the first clickWCAG 4.1.2 (A), HTMLUnit test
checkbox/derived-mixedShouldUsageindeterminate is worked out from the options it stands for whenever they changeOpen ComponentsReview
checkbox/no-immediate-actionShouldUsageChecking a box changes nothing until its form is sent, and a setting that takes effect at once uses a SwitchOpen ComponentsReview
checkbox/requiredShouldBothA box people have to check has required, which lands on the <input>, while optional boxes don'tHTML, WCAG 3.3.2 (A)Unit test, review
checkbox/error-messageMustUsageAn invalid checkbox has an error message, in text and linked with aria-describedby, that says why it needs checkingWCAG 1.4.1, 3.3.1 (A), 3.3.3 (AA)Screen reader
checkbox/invalid-stateMustBothA checkbox sets aria-invalid="true" while its error message shows, and only looks invalid when it doesWCAG 4.1.2 (A), Open ComponentsUnit test
checkbox/read-onlyMustComponentA read-only checkbox sets aria-readonly="true", stays focusable and is sent with its form, while a click, its label or Space never change itWAI-ARIA, Open ComponentsUnit test
checkbox/read-only-valuesShouldUsageA choice people need to see but can't change is read-only rather than disabled, and says why next to itOpen ComponentsReview
checkbox/disabled-inertMustComponentA disabled checkbox, or one inside a <fieldset disabled>, can't be focused, toggled or submitted, and looks disabledHTMLUnit test
checkbox/explain-disabledShouldUsageA disabled checkbox explains why next to it, or is left out until it appliesOpen ComponentsReview
checkbox/focus-not-obscuredMustUsageSticky headers and footers never cover a focused checkbox, for example thanks to scroll-paddingWCAG 2.4.11 (AA), 2.4.12 (AAA)Keyboard
checkbox/mirrors-in-rtlShouldComponentThe box sits at the start of the line in either direction, with logical properties and a grid that follows dirOpen ComponentsReview

FAQ

Checkbox or something else?

A Checkbox says yes or no to one thing. For anything else, another component will do a better job:

When people need to…Use
Say yes or no to one thing, like agreeing to the termsCheckbox
Pick any number of options out of a fewCheckbox Group
Flip a setting that takes effect right away, like Wi-FiSwitch
Pick one option out of a fewRadio Group
Pick one option, or several, out of manySelect, or Combobox
Select rows in a tableA Checkbox in each row, with an aria-label

The Radio Group, the Select and the Combobox are on our Roadmap, and they'll share the Checkbox's sizes, states and rules for labels and errors.

Copyright © 2026