One last time: custom styling radio buttons and checkboxes
About three years ago now (2017/2018), I published a collection of accessible styled form controls which included specific markup patterns to create custom styled radio buttons and checkboxes. These patterns were the culmination of years of my own tinkering, studying other people’s implementations, and then stress testing them with the assistive technologies I had at my disposal.
At the time, the most robust way to style these form controls, without re-creating them from scratch with ARIA, was to visually hide the radio button or checkbox, and then recreate the controls using a <label>
or <span>
and their pseudo-elements (::before
and ::after
). The need for this approach was largely, but not entirely, due to Internet Explorer and Legacy Edge not providing the best support to directly style native HTML <input>
elements themselves. And if you go even further back in time, all browsers had barries in directly styling these controls.
That’s not to say, in 2018, that directly styling native radio buttons and checkboxes couldn’t be done (see restyled radio buttons) and restyled checkboxes). But there were workarounds needed, and styling limitations that still existed due to inconsistencies with Firefox, Internet Explorer, and pre-Chromium Edge, at the time.
Now (2021), with Internet Explorer support being dropped left and right, and Edge now being Chromium-based, and Firefox quirks having been ironed out, these limitations have largely lifted.
I hope this is the last time I write about this subject.
What’s necessary to directly restyle radios and checkboxes?
As we are going to style the elements directly, the expected markup patterns would be the same as if you were not applying custom styling. For instance:
<label for="r1">
<input type="radio" id="r1" name="r">
Restyle Option 1
</label>
<!-- or -->
<input type="radio" id="r2" name="r">
<label for="r2">Resyle option 2</label>
<label for="c1">
<input type="checkbox" id="c1">
Choice 1
</label>
<!-- or -->
<input type="checkbox" id="c2">
<label for="c2">Choice 2</label>
The necessary CSS
There are a variety of CSS properties that radio buttons and checkboxes will respect by default, but to start absolutely fresh (which, if you’re restyling these controls that’s probably what you want anyway), the first thing we need to do is clear them of all default styling. For this, we use the appearance: none
property.
Note: for these particular components, I have not run into any unforeseen issues with using the unprefixed appearance
property. But, according to caniuse.com, Webkit still requires a -webkit-
prefix. Generally best to be safe than sorry, so while annoying to still need both the prefixed -webkit-appearance
and appearance
, might as well keep it up for now.
Once appearance: none
is specified for the radio buttons and checkboxes you’re going to revise, you’ll have full styling control over the element itself, and its ::before
an ::after
pseudo elements. But, now that you’ve taken on this task, you have the following you need to account for in your CSS:
- Default (unchecked state).
- New checked state.
- Optional indeterminiate / ‘mixed’ state (not always necessary, but when needed you’re going to want to have it)
- Custom focus state.
- Checked state focus style (if the previous focus style is not noticeable against the new checked style - e.g., dark outline against a new dark fill when the control is checked).
- Disabled state (make sure the label text continues to meet necessary contrast minimums!).
- Make sure left-to-right and right-to-left (
dir=ltr
,dir=rtl
) was not impacted by any of your styling. - Verify your styling works with Media Queries such as
forced-colors: active
(previously-ms-high-contrast: active
)prefers-color-scheme: dark
prefers-reduced-motion
(if you make some zany animations or something)print
- really any other media query where your custom styling might fall down flat. We should all be testing this stuff anyway, right?
You can play around with the following radio button and checkbox CodePen. Try adding dir=rtl
to the containing <div>
elements, or set one of the controls to disabled
.
See the Pen Custom Styled Native Radio Button by Scott (@scottohara) on CodePen.
Beyond the necessary CSS and markup
Maybe you’re thinking you need more than what’s been called out here. Three selectors to style a radio button or checkbox? “Psh”, you exclaim. “I have some serious user delight that needs injecting into these mundane controls for performing basic tasks on the Internet.” Cool! Feel free to add in other generic/presentational elements as you see fit.
For instance:
<div class="custom-check-container">
<input type="checkbox" id="c">
<span class="blip-bloop" aria-hidden="true"></span>
<span class="bleep-blop" aria-hidden="true"></span>
<label for="c">
Choice
</label>
</div>
<!--
Regarding the aria-hidden=true on the span elements.
You may well not "need" these to be explicitly set to be
hidden from the browser's accessibility tree... but as
I have no idea what one might do with these decorative
elements, seems best to put up some guard rails.
-->
The markup above has wrapped the checkbox within a containing <div>
element, and two extra <span>
elements with silly class names have been added for additional styling flourishes. E.g., want to make an animation effect when hovering, focusing or activating your checkbox? Now you can use CSS sibling selectors (e.g., ~
or +
) and/or some JavaScript to get your Material Design on. Just remember to be careful with your z-indexing so these extra <span>
elements don’t accidentally block a pointer event (i.e., click, touch) from reaching your form control.
Just, whatever you do, you don’t need to do something silly like this:
<!-- this is sloppy. don't be sloppy. -->
<div role=checkbox aria-checked=false tabindex=0 aria-label="Name of control">
<input type=checkbox
tabindex=-1
aria-hidden=true
class=visually-hidden
>
<div>
<!-- oodles of divs go here for reasons that essentially just
boil down to "divs go here until I get the result I want.
Only then will I turn off the div valve." -->
Name of control
</div>
</div>
Nesting interactive elements like the above is absolutely unnecessary, and depending on the assistive technology used, the “hidden” checkbox may still be discoverable (for instance, navigating line-by-line with VoiceOver on macOS, or using Dragon Naturally Speaking - as recent testing has uncovered).
If you find yourself stuck in a situation where you are dealing with the above invalid nesting of a checkbox within a ‘checkbox’, you likely use CSS visibility: hidden
to completely hide the nested native checkbox from assistive technologies, while still maintaining your present functionality. A refactor would be ideal here, as the CSS we ship is a strong suggestion, but not a requirement for users. But hey, sometimes you just gotta hide your mistake and hope that no one peaks under your rug and notices all piles of dirt and dust. Right?
Wrap up and acknowledgements
At this point, I’m seeing very few reasons to continue to recommend the visually-hidden technique to style radio buttons and checkboxes. With all modern browsers being able to handle the styling of these controls, the visually hidden technique has far too many extra gotchas, though manageable, to be aware of and account for.
Additionally, I’m also quite skeptical of the “need” to create custom ARIA radio buttons and checkboxes. The level of effort to make these custom controls, and ensure all necessary functionality is present, goes far beyond just needing to update one’s CSS. Even if you needed to create an ARIA role=switch
, CSS styling and relying on an <input type=checkbox>
’s implicit checked
functionality would give you all that you need to style a native checkbox into a custom switch (the third example on this linked page).
Update 2021-09-25: thanks to Adrian Roselli for reminding me that if I’m going to remind people not to forget styling for all necessary checkbox states, then I shouldn’t forget to include the indeterminiate
/ aria-checked=mixed
state designs. The radio button and checkbox CodePen has been updated to demonstrate examples of checkboxes using the indeterminiate
IDL attribute, and aria-checked=mixed
.
For additional information on styling native radio buttons and checkboxes, particularlly using the visually-hidden technique, I recommend reviewing the following:
- Under-Engineered Custom Radio Buttons and Checkboxen - Adrian Roselli (2017)
- Inclusively Hiding & Styling Checkboxes and Radio Buttons - Sara Soueidan (2020)
- a11y Styled Form Controls: Checkboxes (2018)
- a11y Styled Form Controls: Radio Buttons (2018)
- a11y Styled Form Controls: Star Rating Radio Buttons (2018)
- WTF, Forms (2016)