SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/checkboxes

8 CSS Checkboxes

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.

01. Custom Square Checkbox

FormAccessible

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>
.cbx { display: grid; gap: 14px; color: #1d2433; font: 500 15px Inter, system-ui, sans-serif; }
.cbx label { display: flex; align-items: center; gap: 10px; cursor: pointer; }
.cbx input { appearance: none; flex: none; width: 20px; height: 20px; margin: 0; border: 2px solid #98a2b3; border-radius: 5px; background: #fff center / 14px no-repeat; cursor: pointer; transition: background-color .15s, border-color .15s; }
.cbx input:checked { border-color: #2563eb; background-color: #2563eb; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 8.5l3 3 7-7' fill='none' stroke='white' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
.cbx input:focus-visible { box-shadow: 0 0 0 4px rgba(37,99,235,.25); outline: none; }

02. Round Checkbox

FormCircle

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.

View code
<div class="rnd">
  <label><input type="checkbox" checked><span>Upload product photos</span></label>
  <label><input type="checkbox"><span>Write descriptions</span></label>
  <label><input type="checkbox"><span>Set prices</span></label>
</div>
.rnd { display: grid; gap: 14px; color: #e4e9f5; font: 500 15px Inter, system-ui, sans-serif; }
.rnd label { display: flex; align-items: center; gap: 12px; cursor: pointer; }
.rnd input { appearance: none; position: relative; flex: none; width: 24px; height: 24px; margin: 0; border: 2px solid #5b6478; border-radius: 50%; cursor: pointer; transition: background .2s, border-color .2s; }
.rnd input::after { content: ""; position: absolute; left: 7px; top: 3px; width: 5px; height: 10px; border: solid #0e1118; border-width: 0 2.5px 2.5px 0; transform: rotate(45deg) scale(0); transition: transform .15s; }
.rnd input:checked { border-color: transparent; background: linear-gradient(135deg, #34d399, #22d3ee); }
.rnd input:checked::after { transform: rotate(45deg) scale(1); }
.rnd input:focus-visible { outline: 2px solid #67e8f9; outline-offset: 3px; }

03. To-Do List with Strike-Through

ListState

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.

View code
<ul class="todo">
  <li><label><input type="checkbox" checked><span>Order packaging samples</span></label></li>
  <li><label><input type="checkbox" checked><span>Reply to supplier email</span></label></li>
  <li><label><input type="checkbox"><span>Update shipping rates</span></label></li>
  <li><label><input type="checkbox"><span>Schedule Friday newsletter</span></label></li>
</ul>
.todo { box-sizing: border-box; max-width: 100%; width: min(360px, 90vw); margin: 0; padding: 10px 18px; list-style: none; border-radius: 14px; background: #fff; box-shadow: 0 8px 24px -10px rgba(16,24,40,.25); font: 500 15px Inter, system-ui, sans-serif; }
.todo li { padding: 11px 0; border-bottom: 1px solid #eef0f4; }
.todo li:last-child { border-bottom: 0; }
.todo label { display: flex; align-items: center; gap: 12px; color: #101828; cursor: pointer; }
.todo input { width: 18px; height: 18px; margin: 0; accent-color: #0d9488; cursor: pointer; }
.todo span { background: linear-gradient(#667085, #667085) left center / 0 2px no-repeat; transition: background-size .25s, color .25s; }
.todo label:has(input:checked) span { color: #667085; background-size: 100% 2px; }

04. Selectable Option Cards

CardsMulti-select

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.

View code
<fieldset class="addons">
  <legend>Add extras</legend>
  <label class="addon"><input type="checkbox" checked><b>Gift wrap</b><span>+ $4</span></label>
  <label class="addon"><input type="checkbox"><b>Express shipping</b><span>+ $12</span></label>
  <label class="addon"><input type="checkbox"><b>Extended warranty</b><span>+ $19</span></label>
</fieldset>
.addons { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; width: min(560px, 94vw); margin: 0; padding: 0; border: 0; font-family: Inter, system-ui, sans-serif; }
.addons legend { margin-bottom: 10px; color: #aab3c7; font-size: 13px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
.addon { position: relative; display: grid; gap: 4px; padding: 16px 14px; border: 1.5px solid #2c3346; border-radius: 14px; background: #151a26; color: #e7ebf5; cursor: pointer; transition: border-color .2s, background .2s; }
.addon b { font-size: 15px; }
.addon span { color: #9aa4b8; font-size: 13px; }
.addon input { position: absolute; top: 12px; right: 12px; width: 18px; height: 18px; margin: 0; accent-color: #7c9cff; }
.addon:has(input:checked) { border-color: #7c9cff; background: #1b2240; }
.addon:has(input:focus-visible) { outline: 2px solid #7c9cff; outline-offset: 3px; }
@media (max-width: 520px) { .addons { grid-template-columns: 1fr; } }

05. Select All with Indeterminate State

JavaScriptTree

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>
.tree { display: grid; gap: 10px; margin: 0; padding: 14px 18px 16px 18px; border: 1px solid #e4e7ec; border-radius: 12px; background: #fff; font: 500 15px Inter, system-ui, sans-serif; color: #101828; }
.tree legend { padding: 0 6px; font-weight: 700; }
.tree label { display: flex; align-items: center; gap: 10px; cursor: pointer; }
.tree .child { margin-left: 22px; }
.tree input { appearance: none; flex: none; width: 18px; height: 18px; margin: 0; border: 2px solid #98a2b3; border-radius: 4px; display: grid; place-content: center; cursor: pointer; }
.tree input::before { content: ""; width: 10px; height: 10px; transform: scale(0); background: #fff; clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0, 43% 62%); }
.tree input:checked, .tree input:indeterminate { border-color: #7c3aed; background: #7c3aed; }
.tree input:checked::before { transform: scale(1); }
.tree input:indeterminate::before { transform: scale(1); height: 2.5px; clip-path: none; }
.tree input:focus-visible { outline: 2px solid #7c3aed; outline-offset: 2px; }
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.

View code
<fieldset class="chips">
  <legend>Filter by material</legend>
  <input type="checkbox" id="m1" checked><label for="m1">Cotton</label>
  <input type="checkbox" id="m2"><label for="m2">Linen</label>
  <input type="checkbox" id="m3" checked><label for="m3">Wool</label>
  <input type="checkbox" id="m4"><label for="m4">Recycled</label>
</fieldset>
.chips { display: flex; flex-wrap: wrap; gap: 8px; max-width: 420px; margin: 0; padding: 0; border: 0; font: 600 14px Inter, system-ui, sans-serif; }
.chips legend { width: 100%; margin-bottom: 10px; color: #9aa4b8; font-size: 13px; }
.chips input { position: absolute; opacity: 0; width: 1px; height: 1px; }
.chips label { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; border: 1.5px solid #353d52; border-radius: 999px; color: #d6dcea; cursor: pointer; transition: background .15s, border-color .15s; }
.chips input:checked + label { border-color: #f472b6; background: rgba(244,114,182,.16); color: #fff; }
.chips input:checked + label::before { content: "\2713"; color: #f472b6; font-weight: 800; }
.chips input:focus-visible + label { outline: 2px solid #f9a8d4; outline-offset: 2px; }

07. Animated Drawn Tick

AnimationSVG

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.

View code
<label class="draw">
  <input type="checkbox" checked>
  <svg viewBox="0 0 24 24" aria-hidden="true"><rect x="2" y="2" width="20" height="20" rx="6"/><path d="M7 12.5l3.2 3.2L17 9"/></svg>
  <span>I have read the return policy</span>
</label>
.draw { position: relative; display: inline-flex; align-items: center; gap: 12px; color: #e6eaf4; font: 500 15px Inter, system-ui, sans-serif; cursor: pointer; }
.draw input { position: absolute; left: 0; width: 26px; height: 26px; margin: 0; opacity: 0; cursor: pointer; }
.draw svg { width: 26px; height: 26px; flex: none; }
.draw rect { fill: none; stroke: #5d6680; stroke-width: 2; transition: fill .2s, stroke .2s; }
.draw path { fill: none; stroke: #0e1118; stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round; stroke-dasharray: 16; stroke-dashoffset: 16; transition: stroke-dashoffset .3s .05s; }
.draw input:checked + svg rect { fill: #fbbf24; stroke: #fbbf24; }
.draw input:checked + svg path { stroke-dashoffset: 0; }
.draw input:focus-visible + svg rect { stroke: #fde68a; stroke-width: 3; }
@media (prefers-reduced-motion: reduce) { .draw path, .draw rect { transition: none; } }

08. Required Checkbox with Error

ValidationForm

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>
.terms { box-sizing: border-box; max-width: 100%; display: grid; gap: 10px; width: min(360px, 90vw); padding: 20px; border-radius: 14px; background: #fff; box-shadow: 0 8px 26px -12px rgba(16,24,40,.3); font: 500 15px Inter, system-ui, sans-serif; color: #101828; }
.terms label { display: flex; align-items: center; gap: 10px; cursor: pointer; }
.terms input { width: 18px; height: 18px; margin: 0; accent-color: #2563eb; }
.err { display: none; margin: 0; color: #b42318; font-size: 13px; }
.terms:has(input:user-invalid) .err { display: block; }
.terms:has(input:user-invalid) label { color: #b42318; }
.terms button { justify-self: start; padding: 10px 16px; border: 0; border-radius: 10px; background: #101828; color: #fff; font: 700 14px Inter, sans-serif; cursor: pointer; }
.ok { margin: 0; color: #067647; font-size: 14px; }

About these CSS Checkboxes

appearance: none or accent-color?

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.