How to Style Custom Checkboxes with CSS (Without Breaking Accessibility)
Default checkboxes are ugly and inconsistent across browsers. Chrome renders them one way. Firefox renders them another. Safari does its own thing entirely.
Styling them properly is harder than it should be. You hide the native input. You build a fake one. You forget about keyboard focus. You test with a screen reader and realize it’s broken.
Let’s break down how to do it right.
The HTML Structure
The key is to keep the native <input> in the DOM for accessibility, but visually replace it with a styled element.
html
php-template<label class="custom-checkbox">
<input type="checkbox" />
<span class="checkmark"></span>
Accept terms
</label>The <input> stays accessible to screen readers and keyboards. The <span> becomes the visual checkbox.
The CSS
css
css.custom-checkbox {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}.custom-checkbox input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}.checkmark {
width: 20px;
height: 20px;
border: 2px solid #ccc;
border-radius: 4px;
background: white;
transition: all 0.2s;
}.custom-checkbox input:checked + .checkmark {
background: #4f46e5;
border-color: #4f46e5;
}.custom-checkbox input:focus + .checkmark {
outline: 2px solid #4f46e5;
outline-offset: 2px;
}
Adding a Checkmark Icon
Use a pseudo-element on the checkmark:
css
css.checkmark::after {
content: "";
display: none;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
margin-left: 6px;
margin-top: 2px;
}.custom-checkbox input:checked + .checkmark::after {
display: block;
}For Toggle Switches
Same technique, different shape:
Get KandZ- Tuts’s stories in your inbox
Join Medium for free to get updates from this writer.
css
css.toggle {
width: 44px;
height: 24px;
border-radius: 12px;
background: #ccc;
position: relative;
}.toggle::after {
content: "";
width: 20px;
height: 20px;
border-radius: 50%;
background: white;
position: absolute;
top: 2px;
left: 2px;
transition: transform 0.2s;
}input:checked + .toggle {
background: #4f46e5;
}input:checked + .toggle::after {
transform: translateX(20px);
}
Accessibility Considerations
- Never remove the input from the DOM — use
opacity: 0andposition: absolute - Always include
:focusstyles so keyboard users can see the state - Use
cursor: pointeron the label - Keep the label clickable so the whole area toggles the checkbox
- Test with a screen reader — the input should still announce correctly
Common Gotchas
- The
+sibling selector only works if the styled element comes after the input appearance: noneremoves native styling but keep the input accessible- Dark mode requires separate color variables
- Some browsers render
:focus-visibledifferently than:focus
Prototyping Visually
If you want to design accessible custom checkboxes and toggles without writing all this CSS by hand, I built a free CSS Custom Checkbox Generator. It has live interactive previews and exports clean, accessible code.
100% client-side. Nothing uploaded.
How do you handle custom form controls in your projects? Do you use native inputs or build your own? Let me know in the comments.









