← Back to all tutorials

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

PropertyPurpose
border: solid whiteWhite border on all sides (only right and bottom will be visible)
border-width: 0 2.5px 2.5px 0Zero width on top and left — only right and bottom borders show
width: 6px; height: 12pxTaller 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

FeatureRadio ButtonCheckbox
ShapeCircle (border-radius: 50%)Rounded square (border-radius: 4px)
IndicatorFilled dot (scaled circle)Checkmark (rotated border)
Checked styleDot inside, border changesFill background + white checkmark
Extra stateNoneIndeterminate (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-start so the checkbox aligns with the first line of multi-line labels
  • The :indeterminate pseudo-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