Styling Checkboxes
Build custom checkboxes with CSS — a styled box with an animated checkmark using pseudo-elements and transitions.
Styling Checkboxes
The technique for styling checkboxes is very similar to radio buttons — hide the native input and replace it with a styled element. The difference is the shape (square instead of circle) and the checked indicator (a checkmark instead of a dot). In this episode you will build a custom checkbox with an animated checkmark.
The HTML Structure
<label class="checkbox-label">
<input type="checkbox" id="terms">
<span class="checkbox-custom"></span>
I agree to the terms and conditions
</label>
Same pattern as radio buttons — a <label> wrapping the hidden input, the custom styled span, and the text. Clicking anywhere on the label toggles the checkbox.
Step 1: Hide the Native Checkbox
.checkbox-label input[type="checkbox"] {
position: absolute;
opacity: 0;
width: 0;
height: 0;
pointer-events: none;
}
Step 2: Style the Custom Box
.checkbox-label {
display: flex;
align-items: flex-start;
gap: 10px;
cursor: pointer;
font-size: 15px;
color: #374151;
padding: 8px 0;
line-height: 1.5;
user-select: none;
}
.checkbox-custom {
width: 22px;
height: 22px;
border: 2px solid #d1d5db;
border-radius: 4px;
position: relative;
flex-shrink: 0;
margin-top: 1px;
transition: background 0.2s ease, border-color 0.2s ease;
}
The checkbox is a small square with rounded corners (border-radius: 4px) instead of the full circle used for radio buttons. We use align-items: flex-start so the checkbox aligns with the first line of text when the label wraps.
Step 3: Create the Checkmark
.checkbox-custom::after {
content: "";
position: absolute;
top: 2px;
left: 6px;
width: 6px;
height: 12px;
border: solid white;
border-width: 0 2.5px 2.5px 0;
transform: rotate(45deg) scale(0);
transform-origin: bottom right;
transition: transform 0.15s ease;
}
The checkmark is built from a single pseudo-element using the CSS border trick. By giving only the right and bottom borders a width and rotating 45 degrees, the element looks like a checkmark (✓). It starts at scale(0) and will animate to scale(1) when checked.
How the Checkmark Works
| Property | Purpose |
|---|---|
border: solid white | White border on all sides (only right and bottom will be visible) |
border-width: 0 2.5px 2.5px 0 | Zero width on top and left — only right and bottom borders show |
width: 6px; height: 12px | Taller than wide — creates the elongated checkmark shape |
rotate(45deg) | Tilts the L-shaped border into a checkmark angle |
Step 4: The Checked State
.checkbox-label input[type="checkbox"]:checked + .checkbox-custom {
background: #4A90D9;
border-color: #4A90D9;
}
.checkbox-label input[type="checkbox"]:checked + .checkbox-custom::after {
transform: rotate(45deg) scale(1);
}
When checked, the box fills with the accent color and the checkmark scales up from 0 to 1. The combination of background and border-color changing together with the checkmark appearing creates a satisfying, app-like toggle feel.
Step 5: Hover and Focus States
/* Hover */
.checkbox-label:hover .checkbox-custom {
border-color: #9ca3af;
}
/* Checked + hover */
.checkbox-label input[type="checkbox"]:checked + .checkbox-custom:hover {
background: #357ABD;
border-color: #357ABD;
}
/* Focus (keyboard) */
.checkbox-label input[type="checkbox"]:focus-visible + .checkbox-custom {
outline: 2px solid #4A90D9;
outline-offset: 2px;
}
/* Disabled */
.checkbox-label input[type="checkbox"]:disabled + .checkbox-custom {
border-color: #e5e7eb;
background: #f9fafb;
cursor: not-allowed;
}
.checkbox-label:has(input:disabled) {
color: #9ca3af;
cursor: not-allowed;
}
Indeterminate State
Checkboxes have a third visual state — indeterminate — shown as a horizontal dash. This is used when a "select all" checkbox partially selects items.
.checkbox-label input[type="checkbox"]:indeterminate + .checkbox-custom {
background: #4A90D9;
border-color: #4A90D9;
}
.checkbox-label input[type="checkbox"]:indeterminate + .checkbox-custom::after {
width: 10px;
height: 0;
border-width: 0 0 2.5px 0;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(1);
}
The indeterminate checkmark reuses the same ::after pseudo-element but changes its shape to a horizontal line by zeroing the height and right border.
Toggle Switch Variant
You can extend the checkbox pattern to create a toggle switch:
.toggle-custom {
width: 44px;
height: 24px;
border-radius: 12px;
background: #d1d5db;
transition: background 0.3s ease;
}
.toggle-custom::after {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: 20px;
height: 20px;
border-radius: 50%;
background: white;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
transition: transform 0.3s ease;
transform: scale(1); /* Always visible */
}
input:checked + .toggle-custom {
background: #4A90D9;
}
input:checked + .toggle-custom::after {
transform: translateX(20px);
}
The toggle switch is a wider, pill-shaped checkbox where the indicator is a circle that slides left to right instead of appearing and disappearing.
Checkbox vs Radio Button — CSS Differences
| Feature | Radio Button | Checkbox |
|---|---|---|
| Shape | Circle (border-radius: 50%) | Rounded square (border-radius: 4px) |
| Indicator | Filled dot (scaled circle) | Checkmark (rotated border) |
| Checked style | Dot inside, border changes | Fill background + white checkmark |
| Extra state | None | Indeterminate (dash) |
Key Takeaways
- The CSS checkmark is built with right + bottom borders on a rotated pseudo-element
- The checked state fills the box with color and scales the checkmark from 0 to 1
- Use
align-items: flex-startso the checkbox aligns with the first line of multi-line labels - The
:indeterminatepseudo-class styles the partially-selected state (horizontal dash) - The same pattern extends to toggle switches by changing the shape and animation direction
- Always include hover, focus-visible, and disabled states for a complete component