Skip to main content

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:

  1. Visually hide the native <input> (keep it in the DOM for accessibility).
  2. Style a pseudo-element on the <label> as the visual control.
  3. Use :checked pseudo-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);
}