User Experience (UX)
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-describedbyat itsid. - 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-labelthat says what it selects, likearia-label="Select Ada Lovelace", rather than one that repeats "Select" in every row. - If you name a checkbox with
aria-labeloraria-labelledbywhile 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
| Key | Checkbox |
|---|---|
Tab, Shift+Tab | Moves focus to and from the box |
Space | Checks or unchecks it, and clears the indeterminate state, unless it's read-only |
Enter | Never 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
indeterminateproperty of the<input>. HTML has no attribute for it, so anindeterminateattribute 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 pointaria-describedbyat 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
Enterwith the button already focused. - Put
novalidateon the form, so that the browser's own error bubbles don't compete with yours. Keeprequiredon 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
| Rule | Level | Scope | Requirement | Basis | Check |
|---|---|---|---|---|---|
checkbox/native-element | Must | Component | It'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-toggles | Must | Component | Its <label> wraps the box and its text, so the text names it and a press on the text toggles it, unless it's read-only | WCAG 1.3.1, 4.1.2 (A), HTML | Unit test |
checkbox/visible-label | Must | Usage | Every checkbox has visible text, unless its context names it, like a row in a table, in which case it has an aria-label | WCAG 1.3.1, 3.3.2, 4.1.2 (A) | axe label |
checkbox/label-in-name | Must | Usage | An aria-label or aria-labelledby includes the visible text, ideally at the start | WCAG 2.5.3 (A) | axe label-content-name-mismatch |
checkbox/no-interactive-label | Should | Usage | The label holds no links or buttons, which go in the hint or before the checkbox | Open Components | Review |
checkbox/positive-label | Should | Usage | The label says what checking the box means, in the positive, like "Email me updates" | Open Components | Review |
checkbox/described | Should | Usage | Hints sit outside the label and are linked with aria-describedby | WCAG 1.3.1 (A) | Screen reader |
checkbox/decorative-indicator | Must | Component | The check and the dash are hidden from assistive technologies with aria-hidden, so the name is the label alone | WCAG 1.1.1, 4.1.2 (A) | Unit test |
checkbox/keyboard | Must | Component | Tab reaches the box, Space checks or unchecks it unless it's read-only, and Enter never toggles it | WCAG 2.1.1 (A), APG | Unit test |
checkbox/indeterminate | Must | Component | The mixed state is set as the <input>'s indeterminate property, so it's announced as mixed, and it clears on the first click | WCAG 4.1.2 (A), HTML | Unit test |
checkbox/derived-mixed | Should | Usage | indeterminate is worked out from the options it stands for whenever they change | Open Components | Review |
checkbox/no-immediate-action | Should | Usage | Checking a box changes nothing until its form is sent, and a setting that takes effect at once uses a Switch | Open Components | Review |
checkbox/required | Should | Both | A box people have to check has required, which lands on the <input>, while optional boxes don't | HTML, WCAG 3.3.2 (A) | Unit test, review |
checkbox/error-message | Must | Usage | An invalid checkbox has an error message, in text and linked with aria-describedby, that says why it needs checking | WCAG 1.4.1, 3.3.1 (A), 3.3.3 (AA) | Screen reader |
checkbox/invalid-state | Must | Both | A checkbox sets aria-invalid="true" while its error message shows, and only looks invalid when it does | WCAG 4.1.2 (A), Open Components | Unit test |
checkbox/read-only | Must | Component | A read-only checkbox sets aria-readonly="true", stays focusable and is sent with its form, while a click, its label or Space never change it | WAI-ARIA, Open Components | Unit test |
checkbox/read-only-values | Should | Usage | A choice people need to see but can't change is read-only rather than disabled, and says why next to it | Open Components | Review |
checkbox/disabled-inert | Must | Component | A disabled checkbox, or one inside a <fieldset disabled>, can't be focused, toggled or submitted, and looks disabled | HTML | Unit test |
checkbox/explain-disabled | Should | Usage | A disabled checkbox explains why next to it, or is left out until it applies | Open Components | Review |
checkbox/focus-not-obscured | Must | Usage | Sticky headers and footers never cover a focused checkbox, for example thanks to scroll-padding | WCAG 2.4.11 (AA), 2.4.12 (AAA) | Keyboard |
checkbox/mirrors-in-rtl | Should | Component | The box sits at the start of the line in either direction, with logical properties and a grid that follows dir | Open Components | Review |
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 terms | Checkbox |
| Pick any number of options out of a few | Checkbox Group |
| Flip a setting that takes effect right away, like Wi-Fi | Switch |
| Pick one option out of a few | Radio Group |
| Pick one option, or several, out of many | Select, or Combobox |
| Select rows in a table | A 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.