How to make a checkbox label clickable?
Connect a label to a checkbox so clicking the text toggles it, fix the usual reasons it fails, and style a custom checkbox without breaking keyboard access.
Put the checkbox inside a <label>, or give the checkbox an id and point the label's for attribute at it. Either way, clicking the text toggles the checkbox, and screen readers read the text as the checkbox's name.
<!-- Wrapped: no id needed -->
<label>
<input type="checkbox" name="newsletter">
Send me the newsletter
</label>
<!-- Connected with for and id -->
<input type="checkbox" id="terms" name="terms">
<label for="terms">I accept the terms</label>The same works for radio buttons, and clicking the label of a text input, <select> or <textarea> focuses it. I use the wrapped version for checkboxes: there's no id to keep in sync, and the gap between the box and the text is clickable too. A bigger target also helps on touch screens, so give the label some vertical padding.
Why clicking the label doesn't work
fordoesn't match theid. The match is exact and case-sensitive, sofor="Terms"won't findid="terms".- The id is duplicated. When a component renders twice on one page, both labels point to the first element with that id, and clicking the second label toggles the first checkbox. Generate unique ids (for example with
useId()in React) or use the wrapped version. - You wrote
forin React. In JSX the attribute ishtmlFor, the same wayclassisclassName. - There's a link or button inside the label. Clicking it activates the link or button, not the checkbox. Its text also becomes part of the checkbox's accessible name. Put a "terms" link next to the label, not inside it.
- There are two inputs inside one label. A label is connected to one control only, the first one inside it.
- Something covers the text, like an absolutely positioned element, or
pointer-events: noneis set on the label. - A
clicklistener on the label. When the label wraps the input, one click firesclickon the label and then on the checkbox, which bubbles up to the label again, so the listener runs twice. Callingevent.preventDefault()in it stops the checkbox from toggling. Listen forchangeon the checkbox instead.
To read the state in JavaScript, see how to check if a checkbox is checked.
Styling a custom checkbox
If you only want a different color, accent-color tints native checkboxes, radios and range inputs in all modern browsers:
input[type="checkbox"] {
accent-color: #2563eb;
}For a fully custom look, don't hide the real checkbox with display: none or visibility: hidden and draw a fake one next to it. A hidden input can't be focused, so keyboard users can't reach it. Keep the real input and remove its native look with appearance: none:
<label class="checkbox">
<input type="checkbox" name="updates">
Email me product updates
</label>.checkbox {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding-block: 0.5rem;
cursor: pointer;
}
.checkbox input {
appearance: none;
display: grid;
place-content: center;
width: 1.25rem;
height: 1.25rem;
margin: 0;
border: 2px solid #6b7280;
border-radius: 4px;
background: #fff;
cursor: pointer;
}
/* The checkmark: an L shape rotated 45 degrees */
.checkbox input::before {
content: '';
width: 0.35rem;
height: 0.7rem;
margin-top: -0.15rem;
border: solid #fff;
border-width: 0 2px 2px 0;
rotate: 45deg;
opacity: 0;
}
.checkbox input:checked {
border-color: #2563eb;
background: #2563eb;
}
.checkbox input:checked::before {
opacity: 1;
}
.checkbox input:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}appearance: none removes only the looks. The checkbox still gets Tab focus, toggles with Space, is sent with the form, matches :checked and is announced by screen readers. The :focus-visible style shows keyboard users where they are.
The checkmark is drawn with borders on purpose. Windows forced colors mode (high contrast) replaces background colors, so a checkmark made only from a background color can disappear there, while borders stay visible.