Home / Snippets / UI Components /
Custom checkbox
Styled with appearance: none — smooth transition, real check mark.
Quick implementation
/* HTML: <label><input type="checkbox" class="checkbox" /> Label</label> */
.checkbox {
appearance: none;
-webkit-appearance: none;
width: 1.25rem;
height: 1.25rem;
border: 2px solid oklch(0.45 0.02 260);
border-radius: 0.25rem;
background: transparent;
cursor: pointer;
position: relative;
transition: background 0.15s ease-out, border-color 0.15s ease-out;
}
.checkbox:checked {
background: oklch(0.52 0.22 265);
border-color: oklch(0.52 0.22 265);
}
.checkbox:checked::after {
content: '';
position: absolute;
left: 0.3rem;
top: 0.1rem;
width: 0.35rem;
height: 0.65rem;
border: solid white;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.checkbox:focus-visible {
outline: 2px solid oklch(0.72 0.19 265);
outline-offset: 2px;
}
.checkbox:disabled {
opacity: 0.4;
cursor: not-allowed;
}
Prompt this to your LLM
Includes role, constraints, two framework variants, and edge cases to handle.
You are a senior frontend engineer building custom form controls.
Goal: A styled checkbox with custom check mark using appearance: none — no JavaScript.
Technical constraints:
- Use appearance: none to strip native styling.
- Check mark: ::after pseudo-element with rotated border trick.
- Checked state: background fills with accent color, check mark appears.
- Use oklch() for all colors, not hex or rgba().
- Focus-visible: 2px outline with 2px offset for keyboard navigation.
- Disabled state: reduced opacity, not-allowed cursor.
Framework variant (pick one):
A) Vanilla HTML + CSS only, using native <input type="checkbox">.
B) React component — accept checked, onChange, disabled, label props.
Edge cases to handle:
- Must remain accessible — use real <input> not div, wrapped in <label>.
- Indeterminate state styling if needed (optional).
- Right-to-left layout should not break check mark positioning.
Return HTML + CSS.
Why this matters in 2026
Native checkboxes look different on every OS and browser. appearance: none strips the default styling completely, letting you build a consistent visual across all platforms. Unlike old hidden-checkbox-plus-label hacks, this approach keeps the actual <input> visible and accessible.
The logic
appearance: none removes the browser's default checkbox rendering. The element keeps its semantic behavior — it's still a checkbox, it still toggles, it still works with forms. The ::after pseudo-element on :checked draws the check mark using the border rotation trick: a small rectangle with only a bottom and right border, rotated 45 degrees to form an L-shape that looks like a check mark.
Accessibility & performance
Always wrap the checkbox in a <label> — this makes the entire text clickable and associates the label with the input for screen readers. Never replace the checkbox with a <div> and click handlers. The :focus-visible outline ensures keyboard users can navigate the form. The check mark animation uses opacity and transform, both compositor-friendly.