Form elements like checkbox and radio button look different in different browser and operating system combinations. Therefore designers and developers have been styling them to look consistent, and even better.
In the same time we need to keep checkbox and radio button accessible to assistive technology (AT) and keyboard users. In this article I summarize what does it mean.
Here is the end result for custom checkbox in Github. Markup for one checkbox looks like this:
<div class="checkbox-radio-button-wrapper checkbox-radio-button-wrapper--styled">
<input id="a11y-issue-11" name="a11y-issues-1" type="checkbox" value="no-issues">
<label for="a11y-issue-11">There are no issues</label>
</div>
<div>
wrapper is added for the custom styles but other than that the markup is the same as semantic form markup. Magic happens when we visually hide <input type="checkbox">
using CSS rule opacity: 0
.
.checkbox-radio-button-wrapper--styled {
position: relative;
}
.checkbox-radio-button-wrapper--styled input {
height: 40px;
left: 0;
opacity: 0;
position: absolute;
top: 0;
width: 40px;
}
It's important that we do not hide checkbox using display: none
rule. It would hide checkbox completely from AT users and we would loose keyboard interactions.
The wrapping <div>
have position: relative
CSS rule. That helps us to position checkbox and label ::before
and ::after
pseudo elements where we want using position: absolute
.
We are still missing visual custom checkbox. First we use label::before
element to add border of the checkbox:
.checkbox-radio-button-wrapper--styled input + label::before {
border: 2px solid;
content: "";
height: 40px;
left: 0;
position: absolute;
top: 0;
width: 40px;
}
I have used 2px solid border and inherit color but you could have different border color if needed. Note that we position also this absolute the same way as our "empty" checkbox. And with same height and width to match the styles.
At this point our checkboxes looks like this:
https://www.dropbox.com/s/3dst1q9rdl9jf40/checkbox-styles.png?dl=0
(Alt text: Custom checkbox styles with 2px border.)
Next step is using label::after
pseudo element to style the "check":
.checkbox-radio-button-wrapper--styled input + label::after {
content: "";
border: 4px solid;
border-left: 0;
border-top: 0;
height: 20px;
left: 14px;
opacity: 0;
position: absolute;
top: 6px;
transform: rotate(45deg);
transition: opacity 0.2s ease-in-out;
width: 12px;
}
We create the check using element where we have four pixel border for bottom and right. Then we rotate it 45 degrees, and it looks like our custom check. Also in here you could use different border color to match your design.
https://www.dropbox.com/s/pm7yhkp3yh6enrh/check.png?dl=0
(Alt text: Custom check styles with 4px border bottom and right. When rotating this 45 degrees it looks like a check.)
Note that we're still hiding the check visually using opacity: 0
.
:checked
pseudo selector represent checkbox when it's toggled to "on" state. We use this to change opacity of our custom check:
.checkbox-radio-button-wrapper--styled input:checked + label::after {
opacity: 1;
}
We can also use custom SVG icon for our check. SVG would be inside the label:
<div class="checkbox-radio-button-wrapper checkbox-radio-button-wrapper--styled">
<input id="a11y-issue-2222" name="a11y-issues-3" type="checkbox" value="no-focus-styles">
<label for="a11y-issue-2222">
Focus styles are not present
<svg aria-hidden="true" focusable="false">
// Code for SVG
</svg>
</label>
</div>
In most cases the SVG is just decorative, and aria-hidden="true"
hides it from AT devices.
My example checkbox styles are very similar than in GOV.UK design system for form elements. Focus styles are as important as in any focusable element:
.checkbox-radio-button-wrapper--styled input:focus + label::before {
box-shadow: 0 0 0 3px #ffbf47;
}
We are using box-shadow
for focus styles because that respect round borders as we can see in radio button.
https://www.dropbox.com/s/yjxqwyavjpqhmt9/focus-styles-checkbox.png?dl=0
(Alt text: Focus styles for checkbox: yellow border around checkbox.)
https://www.dropbox.com/s/sqtw9t5xhbpzjff/focus-styles-radio-button.png?dl=0
(Alt text: Focus styles for radio box: yellow round border around radio button.)
Windows High Contrast Mode removes box-shadow rules, that's why we add transparent outline styles:
.checkbox-radio-button-wrapper--styled input:focus + label::before {
box-shadow: 0 0 0 3px #ffbf47;
outline: 3px solid transparent; /* For Windows high contrast mode. */
}
Transparent outline looks like an extra border in high contrast mode.
https://www.dropbox.com/s/zqwc2cqb4bboshz/focus-styles-high-contrast-mode.png?dl=0
(Alt text: Transparent outline shows as border in Windows High Contrast Mode.)
Pretty much all the styles and logic are the same in radio button as in checkbox. Here is the end result for custom radio button in Github.
Only difference is using border-radius and styling checked state a little bit differently. You could certainly use SVG icon in here also. I'll leave that as a homework for you:)
Testing is important part when tinkering native HTML elements. My tests are far from perfect but this is how I tested:
- Keyboard testing in all modern browsers, and IE11.
- Voiceover using Safari.
- NVDA using Firefox.
- Talkback using Android device.
- Color blind conditions using Sim Daltonism software.
- High Contrast Mode in Windows.
In all my tests the custom checkbox and radio button behaved in the same way as native elements.
Test list is missing for example voice recognition software like Dragon, or switch devices.
Hopefully this article gave you examples how to create custom styles for checkbox and radio button, and still maintain the build in accessibility.
I highly recommend learning more about custom form elements from these resources:
- The Accessibility of Styled Form Controls by Scott O'Hara: Lot's of extra examples like star rating, select, and switch.
- GOV.UK form elements.
- Custom checkboxes and radio buttons by Adrian Roselli.