Overview
Custom-styled checkboxes and radio buttons are everywhere in modern UIs, but most implementations replace the native input with a <div> or <span> — destroying keyboard access, form participation, and screen reader semantics. The modern approach is to keep the native <input> and use appearance: none to strip its default look, then apply custom styles directly. This preserves all built-in behavior for free.
WCAG Criteria:
- 4.1.2 Name, Role, Value — interactive elements must expose their role, state, and name
- 1.3.1 Info and Relationships — structure and relationships must be conveyed programmatically
- 2.1.1 Keyboard — all functionality must be operable via keyboard
Key requirements:
- Use native
<input type="checkbox">and<input type="radio">elements - Style with
appearance: none— never replace the input with a<div> - Never use
display: noneorvisibility: hiddenon the native input - Radio groups navigate with Arrow keys (built into native radios)
- The
accent-colorCSS property can quickly customize input colors - Always associate a
<label>viafor/id - Wrap radio groups in
<fieldset>with a<legend>
Custom Checkbox
Custom Checkbox
Inaccessible
<!-- Div with onclick — no keyboard, no role, no form participation -->
<div class="checkbox" onclick="toggleCheck()">
<div class="checkbox-box">
<svg class="check" style="display:none">
<path d="M2 7L5.5 10.5L12 3.5" />
</svg>
</div>
<span>Subscribe to newsletter</span>
</div>Live Preview
Accessible
<!-- Native input with appearance:none — full keyboard + SR support -->
<style>
.custom-checkbox {
appearance: none;
width: 20px;
height: 20px;
border: 2px solid #94a3b8;
border-radius: 4px;
background: #fff;
cursor: pointer;
position: relative;
}
.custom-checkbox:checked {
background: #2563eb;
border-color: #2563eb;
}
.custom-checkbox:checked::after {
content: '';
position: absolute;
left: 5px;
top: 2px;
width: 6px;
height: 10px;
border: solid #fff;
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
.custom-checkbox:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
</style>
<input type="checkbox"
id="subscribe"
class="custom-checkbox" />
<label for="subscribe">Subscribe to newsletter</label>Live Preview
What’s wrong with the div checkbox?
- No
checkboxrole — screen readers announce it as generic text - Not focusable with Tab, cannot be toggled with Space
- Does not participate in form submission
- The
onclickhandler only fires on mouse click, not keyboard - Checked state is purely visual — assistive technology cannot detect it
What the screen reader announces:
| Version | Announcement on focus |
|---|---|
Inaccessible (<div>) | “Subscribe to newsletter” (just text, no role or state) |
Accessible (<input>) | “Subscribe to newsletter, checkbox, not checked” |
| After checking | ”Subscribe to newsletter, checkbox, checked” |
Radio Group
Radio Group
Inaccessible
<!-- Styled divs — no radio role, no grouping, no keyboard -->
<p>Size</p>
<div class="radio-group">
<div class="radio selected" onclick="select(this)">Small</div>
<div class="radio" onclick="select(this)">Medium</div>
<div class="radio" onclick="select(this)">Large</div>
</div>Live Preview
Size
Small
Medium
Large
Accessible
<!-- Native radios with fieldset/legend — Arrow keys work natively -->
<style>
.custom-radio {
appearance: none;
width: 18px;
height: 18px;
border: 2px solid #94a3b8;
border-radius: 50%;
background: #fff;
cursor: pointer;
position: relative;
}
.custom-radio:checked {
border-color: #2563eb;
}
.custom-radio:checked::after {
content: '';
position: absolute;
top: 3px;
left: 3px;
width: 8px;
height: 8px;
border-radius: 50%;
background: #2563eb;
}
.custom-radio:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}
</style>
<fieldset>
<legend>Size</legend>
<input type="radio" name="size"
id="size-s" value="small" checked />
<label for="size-s">Small</label>
<input type="radio" name="size"
id="size-m" value="medium" />
<label for="size-m">Medium</label>
<input type="radio" name="size"
id="size-l" value="large" />
<label for="size-l">Large</label>
</fieldset>Live Preview
What’s wrong with the div radio buttons?
- No
radiorole — screen readers see generic text - No grouping — the relationship between options is not conveyed
- No Arrow key navigation between options (built into native radio groups)
- Selection state is purely visual — assistive technology cannot detect which option is chosen
- Does not participate in form submission
What the screen reader announces:
| Version | Announcement on focus |
|---|---|
Inaccessible (<div>) | “Small” (just text, no role, no group) |
Accessible (<input>) | “Size, group — Small, radio button, 1 of 3, checked” |
| Arrow to next | ”Medium, radio button, 2 of 3” |