Custom Checkboxes and Radio Buttons (Pure CSS)
Browser default checkboxes and radio buttons look different on every OS. This guide shows how to replace them entirely using pure CSS — no SVG, no JS, and fully accessible.
1. The Core Technique
The approach is:
- Visually hide the native
<input>(keep it in the DOM for accessibility). - Style a pseudo-element on the
<label>as the visual control. - Use
:checkedpseudo-class to toggle the visual state.
/* Step 1: Hide the native input but keep it accessible */
.custom-check__input {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
}
2. Custom Checkbox
<label class="custom-check">
<input class="custom-check__input" type="checkbox" id="agree">
<span class="custom-check__box"></span>
<span class="custom-check__label">I agree to the Terms of Service</span>
</label>
.custom-check {
display: inline-flex;
align-items: center;
gap: 0.625rem;
cursor: pointer;
user-select: none;
}
/* The visual box */
.custom-check__box {
position: relative;
flex-shrink: 0;
width: 20px;
height: 20px;
border: 2px solid var(--color-border, rgba(255,255,255,0.15));
border-radius: var(--radius-sm, 4px);
background: transparent;
transition: background 0.2s, border-color 0.2s, box-shadow 0.2s;
}
/* The checkmark (hidden by default) */
.custom-check__box::after {
content: '';
position: absolute;
top: 2px;
left: 6px;
width: 5px;
height: 10px;
border: 2px solid white;
border-top: 0;
border-left: 0;
transform: rotate(45deg) scale(0);
transition: transform 0.15s cubic-bezier(0.12, 0.4, 0.29, 1.46);
}
/* Checked state */
.custom-check__input:checked + .custom-check__box {
background: var(--color-primary);
border-color: var(--color-primary);
}
.custom-check__input:checked + .custom-check__box::after {
transform: rotate(45deg) scale(1);
}
/* Focus state (keyboard navigation) */
.custom-check__input:focus-visible + .custom-check__box {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(108, 99, 255, 0.2);
}
/* Disabled state */
.custom-check__input:disabled + .custom-check__box { opacity: 0.4; cursor: not-allowed; }
.custom-check__input:disabled ~ .custom-check__label { opacity: 0.4; cursor: not-allowed; }
3. Indeterminate State (Three-State Checkbox)
/* Used for "select all" parent checkboxes */
.custom-check__input:indeterminate + .custom-check__box {
background: var(--color-primary);
border-color: var(--color-primary);
}
.custom-check__input:indeterminate + .custom-check__box::after {
top: 8px;
left: 3px;
width: 10px;
height: 0;
border-left: 0;
border-top: 0;
border-right: 0;
border-bottom: 2px solid white;
transform: rotate(0) scale(1);
}
4. Custom Radio Button
<fieldset class="radio-group">
<legend>Select a plan</legend>
<label class="custom-radio">
<input class="custom-radio__input" type="radio" name="plan" value="basic">
<span class="custom-radio__dot"></span>
<span class="custom-radio__label">Basic — Free</span>
</label>
<label class="custom-radio">
<input class="custom-radio__input" type="radio" name="plan" value="pro" checked>
<span class="custom-radio__dot"></span>
<span class="custom-radio__label">Pro — $12/mo</span>
</label>
</fieldset>
.radio-group {
display: flex;
flex-direction: column;
gap: 0.75rem;
border: none;
padding: 0;
}
.radio-group legend {
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--color-text);
}
.custom-radio {
display: inline-flex;
align-items: center;
gap: 0.625rem;
cursor: pointer;
}
.custom-radio__input {
position: absolute;
opacity: 0; width: 1px; height: 1px;
margin: -1px; overflow: hidden;
clip: rect(0,0,0,0); white-space: nowrap;
}
/* Outer circle */
.custom-radio__dot {
flex-shrink: 0;
width: 20px;
height: 20px;
border: 2px solid var(--color-border, rgba(255,255,255,0.15));
border-radius: 50%;
background: transparent;
position: relative;
transition: border-color 0.2s, box-shadow 0.2s;
}
/* Inner dot */
.custom-radio__dot::after {
content: '';
position: absolute;
top: 50%; left: 50%;
width: 8px; height: 8px;
border-radius: 50%;
background: white;
transform: translate(-50%, -50%) scale(0);
transition: transform 0.15s cubic-bezier(0.12, 0.4, 0.29, 1.46);
}
.custom-radio__input:checked + .custom-radio__dot {
border-color: var(--color-primary);
background: var(--color-primary);
}
.custom-radio__input:checked + .custom-radio__dot::after {
transform: translate(-50%, -50%) scale(1);
}
.custom-radio__input:focus-visible + .custom-radio__dot {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
5. Toggle / Switch (Checkbox Variant)
A pill-shaped on/off toggle:
<label class="toggle">
<input class="toggle__input" type="checkbox" id="notifications">
<span class="toggle__track">
<span class="toggle__thumb"></span>
</span>
<span class="toggle__label">Enable Notifications</span>
</label>
.toggle {
display: inline-flex;
align-items: center;
gap: 0.75rem;
cursor: pointer;
}
.toggle__input {
position: absolute; opacity: 0; width: 1px; height: 1px;
margin: -1px; overflow: hidden; clip: rect(0,0,0,0);
}
.toggle__track {
position: relative;
width: 44px;
height: 24px;
border-radius: 9999px;
background: var(--color-border, rgba(255,255,255,0.1));
border: 2px solid transparent;
transition: background 0.25s, border-color 0.25s;
}
.toggle__thumb {
position: absolute;
top: 2px; left: 2px;
width: 16px; height: 16px;
border-radius: 50%;
background: var(--color-text-muted, #888);
transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.25s;
}
.toggle__input:checked + .toggle__track {
background: var(--color-primary, #6c63ff);
}
.toggle__input:checked + .toggle__track .toggle__thumb {
transform: translateX(20px);
background: white;
}
.toggle__input:focus-visible + .toggle__track {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
6. Checkbox Card (Full-Area Clickable)
<label class="checkbox-card">
<input type="checkbox" class="checkbox-card__input">
<span class="checkbox-card__body">
<strong>Pro Plan</strong>
<span>All features included</span>
</span>
</label>
.checkbox-card { display: block; cursor: pointer; }
.checkbox-card__input { position: absolute; opacity: 0; width: 0; height: 0; }
.checkbox-card__body {
display: flex; flex-direction: column; gap: 0.25rem;
padding: 1rem 1.25rem;
border: 2px solid var(--color-border);
border-radius: var(--radius-md);
transition: border-color 0.2s, box-shadow 0.2s;
}
.checkbox-card__input:checked + .checkbox-card__body {
border-color: var(--color-primary);
box-shadow: 0 0 0 4px rgba(108, 99, 255, 0.15);
}