7 Pure CSS1 with JSPublished 2026-10-04Free · no sign-up
Eight custom checkbox styles that keep the native input underneath, so they stay accessible and work in forms. They range from a crisp square box and a round task checkbox to selectable cards, filter chips and a tick that draws itself.
A crisp custom checkbox made from the native input with appearance: none, so it keeps keyboard support, form submission and the checked state for free. The tick is an inline SVG data URL that fades in, and the box grows a soft ring on focus.
View code
<div class="cbx">
<label><input type="checkbox" checked> Remember me on this device</label>
<label><input type="checkbox"> Send me product news</label>
</div>
Circular checkboxes suit task lists and photo pickers. The check mark is two borders on a rotated pseudo-element, so it scales with the box and inherits its colour, and the circle fills with a gradient once it is selected.
A to-do list where ticking an item crosses it out and dims it. The label text reacts to its own checkbox with the :has() selector, so the styling stays in CSS and the list keeps working if JavaScript fails to load.
Larger hit areas for multi-select choices such as add-ons. Each whole card is the label, the checkbox sits in the corner, and the selected card gets a border and tint through :has(), which also works for keyboard users moving with Tab and Space.
A parent checkbox that shows a dash when only some children are ticked. The indeterminate state can only be set from JavaScript, so a few lines keep the parent and children in sync in both directions; the styling uses the :indeterminate pseudo-class.
View code
<fieldset class="tree">
<legend><label><input type="checkbox" id="all"> All regions</label></legend>
<label><input type="checkbox" class="child" checked> Europe</label>
<label><input type="checkbox" class="child"> North America</label>
<label><input type="checkbox" class="child" checked> Asia Pacific</label>
</fieldset>
const all = document.getElementById('all');
const kids = [...document.querySelectorAll('.child')];
function sync() {
const n = kids.filter(k => k.checked).length;
all.checked = n === kids.length;
all.indeterminate = n > 0 && n < kids.length;
}
all.addEventListener('change', () => { kids.forEach(k => k.checked = all.checked); sync(); });
kids.forEach(k => k.addEventListener('change', sync));
sync();
06. Filter Chip Checkboxes
FilterChips
Product filters shown as chips. Each chip is a visually hidden checkbox plus its label, so the whole chip toggles and screen readers still hear a checkbox. Selected chips gain a tick and a filled background, and the group wraps naturally on small screens.
The tick draws itself when the box is checked. An SVG path sits over the hidden input, and its stroke-dashoffset animates from the full path length to zero; with reduced motion turned on, the tick simply appears without the drawing effect.
A terms-and-conditions checkbox that must be ticked before submitting. The required attribute does the checking, :user-invalid shows the red state only after the person has tried to submit, and the error text is linked with aria-describedby.
View code
<form class="terms" onsubmit="event.preventDefault(); this.querySelector('.ok').hidden = false;">
<label><input type="checkbox" required aria-describedby="t-err"> I agree to the terms of service</label>
<p class="err" id="t-err">Please accept the terms to continue.</p>
<button>Create account</button>
<p class="ok" hidden>Thanks, your account is ready.</p>
</form>
For a quick brand colour, accent-color recolours the native checkbox in one line and keeps the platform look. For a fully custom box, appearance: none removes the default drawing so you can style the input with borders, backgrounds and pseudo-elements, while the browser still handles checking, focus and submission.
Making the whole card clickable
Wrapping the input and its text in one <label> makes the entire area clickable. The :has(input:checked) selector then styles the label when its own checkbox is ticked, which is how the option cards and the to-do list work without JavaScript.
The indeterminate state
A select-all checkbox shows a dash when only some items are ticked. That state can only be set with JavaScript through checkbox.indeterminate = true; it is not an HTML attribute. Style it with :indeterminate and recompute it whenever a child changes, as the select-all demo does.
Required checkboxes
For terms and consent boxes, add required and let the browser validate. The :user-invalid pseudo-class only matches after the person has interacted or tried to submit, so you can show an error without making the form red before anyone has touched it.
Frequently asked questions
Will these work in all browsers?
Yes in current Chrome, Edge, Firefox and Safari. :has() and :user-invalid are supported in all of them since 2023.
Do I need JavaScript?
Only the select-all demo uses JavaScript, because the indeterminate state cannot be set in HTML.
How do I change the tick colour?
Change the background colour in the :checked rule, or set accent-color for native checkboxes.