I've seen a ton of designers make these GORGEOUS checkbox styles, but then you see them implemented and you can't even select it using your keyboard. Let's say we got this in our style guide from our designer.
I've seen this implemented before and it looks gorgeous. However, when I press the tab
key, it zips right past it. If this field is required, you're screwing over a bunch of your users. They use ::before
or ::after
pseudo-elements to make a pretty checkbox and use the :checked
pseudo-class to determine the styling of the check itself. It looks cool, but the problem is that they use display: none
on the checkbox input itself. When we do that, we make the checkbox itself invisible to the browser, making it unusable for those who rely on keyboards to navigate a site.
Starting point
Let's walk step by step how I would go through this. Here is what my starting code looks like:
<fieldset>
<legend>Accessible Checkboxes</legend>
<input type="checkbox" name="Checkbox" id="check_1">
<label for="check_1">Checkbox</label>
<input type="checkbox" name="CSS Only" id="css_only">
<label for="css_only">CSS Only</label>
<input type="checkbox" name="" id="disabled_sample" disabled>
<label for="disabled_sample">A disabled checkbox</label>
<input type="checkbox" name="Fourth Option" id="fourth_check">
<label for="fourth_check">Fourth Option</label>
</fieldset>
I would start with a bare-bones checkbox list. Here is the current CSS I have:
input[type="checkbox"] {
position: absolute;
}
input[type="checkbox"] + label {
display: block;
position: relative;
padding: 0 1.5rem;
}
Create a pseudo-element on the label
The first thing I want to do is make sure that I create a pseudo-element that can act in place of my checkbox. What I'll do to achieve this is create a ::before
pseudo-element on the <label>
element. Now it looks like this:
input[type="checkbox"] + label::before {
content: '';
position: relative;
display: inline-block;
margin-right: 10px;
width: 20px;
height: 20px;
background: white;
}
I've left the non-styled original checkbox there on purpose. The reason for this is it makes it easier for me to tell when a checkbox is focused, checked, etc. It helps me to hold off on hiding the checkbox until the very last minute.
Add styling on the pseudo-element when checked
As of right now, when we try to check the checkbox, it doesn't do anything except the normal behavior. What we have to do is add a little bit of CSS magic using the :checked
pseudo-class. See Below:
input[type="checkbox"]:checked + label::before {
background: #5ac5c9;
}
Add your custom checkmark
If you want to do a checkmark unicode to the ::before
element's content, you can very well do that. However, I want to get a little fancy. Now, we want to make sure that there is a perpendicular checkmark inside of our custom element. I've done this by adding an ::after
pseudo-element. What we are doing here is creating a right angle with two borders and rotating it.
input[type="checkbox"]:checked + label::after {
content: '';
position: absolute;
top: 3px;
left: 27px;
border-left: 2px solid black;
border-bottom: 2px solid black;
height: 6px;
width: 13px;
transform: rotate(-45deg);
}
An additional challenge, instead of a check, make an "X."
Add focus styles to the pseudo-element
Great! Are we good to go now? Well, not quite.
We still need to ensure that the pseudo-element "receives focus." What we are going to do now is replicate the focus styling on when the checkbox receives focus. The reason why we don't want to do display: none
is because removing the display prevents the checkbox from receiving focus at all. I wanted to have some concrete focus styling since they can vary from browser to browser. Below is what I ended up doing because I wanted to replicate the default focus for Chrome, but in all browsers. It's not the same, but it's close!
input[type="checkbox"]:focus + label::before {
outline: #5d9dd5 solid 1px;
box-shadow: 0 0px 8px #5e9ed6;
}
Now we can hide it the original checkbox! See how helpful keeping it around when we were figuring this out?
input[type="checkbox"] {
position: absolute;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
clip: rect(1px, 1px, 1px, 1px);
}
(The above code is the common visually hidden css)
Add some styling for the disabled checkboxes
One last thing, we should probably make that disabled checkbox stylistically different. Below is what I did:
input[type="checkbox"]:disabled + label {
color: #575757;
}
input[type="checkbox"]:disabled + label::before {
background: #ddd;
}
So that's it! You can apply the same principles to radio buttons as well. Let me know on Twitter or comment below what you think!
Disclaimer: I almost didn't post this. Last night as I was scrolling through Twitter, I saw a post that has almost the same tips in it. After asking the Twitterverse if I should post it, I got an overwhelming amount of people saying "YES!" I haven't read the other one, only skimmed, mostly because I didn't want to impact my writing. However, I wanted to link it up for you, in case you wanted to read it: How to Make Custom Accessible Checkboxes and Radio Buttons.
Top comments (19)
I will always upvote a11y content, I think it's so important.
I'm on mobile so I'll try and keep this short
One thing I would change with your code would be to not paint the check every time. To do this I'd change the CSS to something like
It's a lot easier for the browser to change the opacity than it is to make the tick anew each time. Though it's fair to say no one would notice any improvement in performance I just prefer to do it that way as a style preference.
Hmm! That's a good point, I also like that :).
Right, now that I'm on a desktop this is how I tend to do things
jsfiddle.net/link2twenty/3nrczx2q/
The main difference is the way I handle the HTML
I do it this way so you don't have to have an ID for each checkbox.
Are you planning on turning this into a series for different input types?
I have to read more into this and do some testing, I've always been yelled at for not having associations between inputs and labels, but I can see why having an ID for every checkbox may get annoying.
I have to sign into the day job, but I wanna look into this tonight and get back to you :)
Also, can I say the fact that you typed out the code on your phone is impressive!
Great tips! You probably already know this, but I think accessibility is overlooked. And I've made my fair share of custom-styled checkboxes because that's what our designers wanted. Now I know better and can still build it the way they want.
Hey Jimmy :D.
Yeah! I may change the CSS of this post to use
visually-hidden
for the checkbox itself based on a few comments I've seen. Just note that so you can come back to it :DThanks for this! Checkboxes are not a straightforward html element.
They aren't! The good news is once you get the hang of it, it takes no time at all to implement. I fixed this in < 10 minutes on a site I was temporarily helping out with.
Case in point, people have other ways to do things in the comments and now I have more things to check out :P
Hi! great article. But have you tried this out on iPhone? left: -99999px might be considered as out of view port, so clicking on checkbox might not work.
clip: rect(0,0,0,0);
may solve this instead ofleft
Yeah, I used to use clip more, must have slipped my brain.
The good news though is you're thinking about ensuring that the checkbox will work, both minds in the right place :D
I like doing
That's a good point! Need to test it more! Signing into the day job now, but will report back!
I've written an article inspired by this one. I'd love to hear your thoughts 🙂
Sweet! I will take a look! Sorry I haven't gotten back to this post sooner, so much going on this week!
No rush, just thought I'd let you know 🙂
Awesome post! Thanks for sharing!