Skip to Content
Component ExamplesCustom Checkboxes & Radios

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:

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: none or visibility: hidden on the native input
  • Radio groups navigate with Arrow keys (built into native radios)
  • The accent-color CSS property can quickly customize input colors
  • Always associate a <label> via for/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
Subscribe to newsletter
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 checkbox role — screen readers announce it as generic text
  • Not focusable with Tab, cannot be toggled with Space
  • Does not participate in form submission
  • The onclick handler only fires on mouse click, not keyboard
  • Checked state is purely visual — assistive technology cannot detect it

What the screen reader announces:

VersionAnnouncement 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
Size

What’s wrong with the div radio buttons?

  • No radio role — 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:

VersionAnnouncement 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”

Resources