accent-color: #4f46e5 to your checkboxes and radios. For a fully custom look, set appearance: none on the input, give it a size, border and radius, and draw the tick with a ::before pseudo-element that scales in on :checked. In Tailwind use accent-indigo-600, or appearance-none with checked: and peer-checked: variants. Never hide the input with display: none.Point any HTML form at one endpoint. 500 free submissions — no card, no time limit — and unlimited forms.
Create free accountThe quick fix: accent-color
If you only want the checked state to use your brand colour instead of the default blue, one CSS line does it. It works on checkboxes, radio buttons, range sliders and progress bars:
input[type="checkbox"],
input[type="radio"] {
accent-color: #4f46e5;
}
/* Optional: make the native box bigger */
input[type="checkbox"] {
width: 1.25rem;
height: 1.25rem;
}accent-color is supported in Chrome and Edge 93+, Firefox 92+ and Safari 15.4+, so every current browser. Older browsers simply show the default colour, so it is safe to use with no fallback. The browser keeps the native look, focus ring, keyboard behaviour and high-contrast support, and it picks a white or black tick that contrasts with your colour automatically. The box is a standard HTML input with type="checkbox", so its value and submitted state follow the same rules as any other input.
What you can't change with accent-color: the border, the corner radius, the tick shape or the tick colour. For those you need the custom box below.
A fully custom checkbox with appearance: none
appearance: none removes the browser's drawing of the box but keeps the real input: it still gets focus, still toggles with the Space key, still submits its value and is still announced by screen readers. You then style the input itself like any element:
.checkbox {
-webkit-appearance: none;
appearance: none;
margin: 0;
width: 1.25rem;
height: 1.25rem;
border: 2px solid #6b7280;
border-radius: 0.3rem;
background: #fff;
display: grid;
place-content: center;
cursor: pointer;
}
/* The checkmark, drawn with clip-path */
.checkbox::before {
content: "";
width: 0.7rem;
height: 0.7rem;
background: #fff;
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
transform: scale(0);
transition: transform 120ms ease-in-out;
}
.checkbox:checked {
background: #4f46e5;
border-color: #4f46e5;
}
.checkbox:checked::before {
transform: scale(1);
}<label>
<input type="checkbox" class="checkbox" name="newsletter" value="yes" />
Send me the newsletter
</label>display: grid; place-content: center centres the pseudo-element inside the box without any absolute positioning. The clip-path polygon cuts a tick shape out of a square, so the tick colour is just its background. Scaling from 0 to 1 gives a small pop-in animation; add @media (prefers-reduced-motion: reduce) and set transition: none if you want to respect that setting.
Pseudo-elements on an <input> only render once its native appearance is removed, which is why ::before works here and does nothing on a default checkbox.
The radio button version
A custom radio is the same pattern with a circle: border-radius: 50% on the input and a round dot as the ::before.
.radio {
-webkit-appearance: none;
appearance: none;
margin: 0;
width: 1.25rem;
height: 1.25rem;
border: 2px solid #6b7280;
border-radius: 50%;
background: #fff;
display: grid;
place-content: center;
cursor: pointer;
}
.radio::before {
content: "";
width: 0.6rem;
height: 0.6rem;
border-radius: 50%;
background: #4f46e5;
transform: scale(0);
transition: transform 120ms ease-in-out;
}
.radio:checked {
border-color: #4f46e5;
}
.radio:checked::before {
transform: scale(1);
}<fieldset>
<legend>Plan</legend>
<label><input type="radio" class="radio" name="plan" value="free" checked /> Free</label>
<label><input type="radio" class="radio" name="plan" value="pro" /> Pro</label>
</fieldset>The arrow keys still move between radios in the same name group, because the inputs are real. Group them in a fieldset with a legend so screen readers announce the question.
States: indeterminate, disabled and focus-visible
Once you draw the box yourself, you also have to draw every state the browser used to handle:
/* Indeterminate: a dash instead of a tick */
.checkbox:indeterminate {
background: #4f46e5;
border-color: #4f46e5;
}
.checkbox:indeterminate::before {
clip-path: none;
height: 0.15rem;
transform: scale(1);
}
/* Disabled */
.checkbox:disabled,
.radio:disabled {
border-color: #d1d5db;
background: #f3f4f6;
cursor: not-allowed;
}
.checkbox:disabled + span,
.radio:disabled + span {
color: #9ca3af;
}
/* Keyboard focus ring */
.checkbox:focus-visible,
.radio:focus-visible {
outline: 2px solid #4f46e5;
outline-offset: 2px;
}Indeterminate has no HTML attribute. It is a JavaScript property, used for a "select all" box when only some children are ticked:
const all = document.querySelector('#select-all');
all.indeterminate = true; // shows the dash; clicking clears it:focus-visible shows the outline for keyboard users only, not after a mouse click. Don't remove the outline without replacing it, or keyboard users can't see where they are.
Never use display: none on the input. A common old trick hides the checkbox and styles a <span> next to it. With display: none or visibility: hidden, the input leaves the tab order and the accessibility tree: keyboard users can't reach it and screen readers don't announce it. If you really need a sibling element, hide the input visually instead:
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
}A toggle switch built from a checkbox
A switch is a checkbox that looks like a slider. Add role="switch" so screen readers announce it as "switch, on" or "off" instead of "checkbox, checked":
<label class="switch-label">
<input type="checkbox" role="switch" class="switch" name="notifications" value="on" />
Email notifications
</label>
<style>
.switch {
-webkit-appearance: none;
appearance: none;
margin: 0;
width: 2.75rem;
height: 1.5rem;
border-radius: 999px;
background: #d1d5db;
position: relative;
cursor: pointer;
transition: background 150ms;
}
.switch::before {
content: "";
position: absolute;
top: 0.125rem;
left: 0.125rem;
width: 1.25rem;
height: 1.25rem;
border-radius: 50%;
background: #fff;
transition: transform 150ms;
}
.switch:checked {
background: #4f46e5;
}
.switch:checked::before {
transform: translateX(1.25rem);
}
.switch:focus-visible {
outline: 2px solid #4f46e5;
outline-offset: 2px;
}
</style>It submits exactly like a checkbox: notifications=on when on, and nothing at all when off.
Tailwind: accent-indigo-600 and peer-checked
The quick fix in Tailwind is the accent-* utility:
<input type="checkbox" name="terms" class="size-5 accent-indigo-600" />For a custom box, use appearance-none on the input with checked: variants, and put the tick icon in a sibling that reacts to peer-checked::
<label class="relative inline-flex items-center gap-2">
<input
type="checkbox"
name="topics"
value="design"
class="peer size-5 appearance-none rounded border-2 border-gray-500 bg-white
checked:border-indigo-600 checked:bg-indigo-600
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600
disabled:border-gray-300 disabled:bg-gray-100"
/>
<svg
class="pointer-events-none absolute left-0.5 top-1/2 size-4 -translate-y-1/2 text-white opacity-0 peer-checked:opacity-100"
viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"
>
<path d="M3 8.5l3 3 7-7" />
</svg>
Design
</label>peer marks the input, and peer-checked:opacity-100 shows the SVG only when the input before it is checked. The SVG has pointer-events-none so clicks go through to the input, and aria-hidden so it isn't read out. The peer must come before the element that uses peer-*. In React, use className and strokeWidth.
Accessibility checklist
- Keep the real input. Restyle it with
appearance: noneor hide it visually; never replace it with a<div>. - Label every box. Wrap it in a <label>, or use
forwith the input'sid, so clicking the text toggles it and screen readers read the name. - Make the target big enough. WCAG 2.2 asks for a 24 by 24 CSS pixel target (2.5.8). A 20px box inside a clickable label easily passes.
- Check contrast. The unchecked border needs a 3:1 contrast ratio against the background (1.4.11), and so does the tick against the fill.
- Show focus. Keep a visible
:focus-visibleoutline. - Support forced colors. Windows High Contrast mode drops backgrounds, so a tick drawn with
backgroundcan vanish. Give it a system colour:
@media (forced-colors: active) {
.checkbox,
.radio {
border-color: CanvasText;
}
.checkbox::before,
.radio::before {
background: CanvasText;
}
.checkbox:checked,
.radio:checked {
background: Canvas;
}
}Working example: a styled checkbox group that sends to your inbox
Paste this into any page. It posts to splitforms, so each submission lands in your email and dashboard without writing a backend. Every ticked box sends its own topics value:
<form action="https://splitforms.com/api/submit" method="POST">
<input type="hidden" name="access_key" value="YOUR_ACCESS_KEY" />
<label for="email">Email</label>
<input id="email" name="email" type="email" required />
<fieldset>
<legend>What are you interested in?</legend>
<label class="option"><input type="checkbox" class="checkbox" name="topics" value="design" /> Design</label>
<label class="option"><input type="checkbox" class="checkbox" name="topics" value="development" /> Development</label>
<label class="option"><input type="checkbox" class="checkbox" name="topics" value="marketing" /> Marketing</label>
</fieldset>
<button type="submit">Send</button>
</form>
<style>
.option { display: flex; align-items: center; gap: 0.5rem; min-height: 2rem; cursor: pointer; }
.checkbox {
-webkit-appearance: none;
appearance: none;
margin: 0;
width: 1.25rem;
height: 1.25rem;
border: 2px solid #6b7280;
border-radius: 0.3rem;
background: #fff;
display: grid;
place-content: center;
cursor: pointer;
}
.checkbox::before {
content: "";
width: 0.7rem;
height: 0.7rem;
background: #fff;
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
transform: scale(0);
transition: transform 120ms ease-in-out;
}
.checkbox:checked { background: #4f46e5; border-color: #4f46e5; }
.checkbox:checked::before { transform: scale(1); }
.checkbox:focus-visible { outline: 2px solid #4f46e5; outline-offset: 2px; }
@media (forced-colors: active) {
.checkbox { border-color: CanvasText; }
.checkbox::before { background: CanvasText; }
.checkbox:checked { background: Canvas; }
}
</style>Get a free access key to try it. Unticked boxes send nothing, so to record a "no" see how to send an unchecked checkbox value. To make the group mandatory, see require at least one checkbox.
FAQ
Why doesn't accent-color change the size of the checkbox?
accent-color only changes the colour the browser uses for the checked fill and the tick. Size is a separate thing: set width and height on the input (for example width: 1.25rem; height: 1.25rem). Chrome, Edge and Firefox scale the native box to that size. For full control over size, border and radius, use appearance: none and draw the box yourself.
How do I change the checkmark color of a checkbox?
With accent-color the browser picks a tick colour (white or black) that contrasts with your accent, and you can't set it directly. To choose the tick colour yourself, use appearance: none and draw the tick with a ::before pseudo-element; its background-color (or box-shadow) is the checkmark colour.
How do I style a checkbox in Safari?
Safari supports accent-color from version 15.4 and the unprefixed appearance: none from 15.4 as well. For older Safari add -webkit-appearance: none next to appearance: none. Safari also needs the input to be a real element with width and height set, because ::before on an input only renders once its native look is removed.
Should I use a checkbox or a switch?
Use a checkbox when the choice takes effect after the form is submitted, such as agreeing to terms or picking topics. Use a switch (a checkbox with role="switch") when the setting applies right away, like turning notifications on. Both submit name=value only when on.
Can I hide the checkbox with display: none and style the label?
No. display: none (and visibility: hidden) removes the input from the keyboard tab order and from screen readers, so the control can't be reached or announced. Keep the real input on the page and restyle it with appearance: none, or visually hide it with a clip pattern and style a sibling with :checked + span.
References: MDN: accent-color, MDN: appearance and MDN: forced-colors.