Checkbox Trickery with CSS
The Basic Formula
It all starts with the HTML.
Nothing tricky there. The
for attribute on
<label> matches the
<input>, so clicking on the
<label> will toggle the
<input> checkbox. This is important, because our next step is to hide
display: none? Because that would cause it to be ignored by screen readers and keyboard tabbing. This method keeps
<input> in the flow, but hides it offscreen.
<input> makes it easier for us to do our own thing. We still need to convey the checked/unchecked state, but we can do that with
<label>. This is where the party starts.
We’re using a combination of the
:checked pseudo-class and the
+ adjacent sibling selector to say “when the checkbox is checked, find the
<label> right after it and apply awesome styles”. You can even use pseudo-elements (
<label> for more creative freedom.
Alright, let’s see it in action. This demo uses the basic formula we just discussed to turn regular checkboxes into something more impressive.
The best part is, they’re still checkboxes. Wrap them in a
<form> and they’ll submit just like you’d expect. We’re changing appearances, but not behavior.
So far it’s been all about styling
<label>, but we can go beyond that. This demo dynamically hides/shows parts of a form based on user selection.
:checked pseudo-class works on radio buttons the same as checkboxes. With that in mind, here’s the HTML for the “How did you hear about us?” radio buttons.
The radio button indicators are rendered within
<label> using a combination of
::before (for the outer ring) and
::after (for the green dot). Showing/hiding
::after when a radio button is checked/unchecked is easy enough.
<div> is hidden until the radio button for “Other…” is checked. I hide the
display: none, because this time I do want the content to be ignored by screen readers and keyboard tabbing until revealed. The CSS to reveal the
<div> when the radio button is checked looks like this.
We’ve been using the
+ adjacent sibling selector up until now, but this time we’re using the
~ general sibling selector. It’s similar, but can find non-adjacent siblings, like our
We can reuse the techniques from the previous demo to create a folder tree widget, which has similar hide/show behavior.
The HTML for a single folder is given below. The
<label> is the folder and the two
<a> elements are “files” within it.
Font Awesome icons are used to indicate checked (open) and unchecked (closed) states.
Content within a folder is toggled with a
~ general sibling selector. This is why there are extra
<div> wrappers in the HTML, to keep this selector from “leaking out” and opening sibling folders.
Naturally, folders can be nested. Just drop the HTML for another folder into
<div class="sub">. Click the “Multicolor” folder in the demo to see an example.
Lastly, let’s talk about that reset button.
Form reset buttons are rarely used anymore, but this is a decent case for one. Clicking it returns all checkboxes to their initial unchecked state, closing all folders. Neat.
This demo splits items into two separate lists depending on whether they’re checked done or not.
Update: Some people have pointed out that the convention is for checkboxes to be square, unlike what I’ve done in this demo. They’re not wrong.
The HTML looks like this.
The split list mechanic is achieved with CSS flexbox. Here’s the relevant CSS.
CSS flexbox lets you directly rearrange elements with the
order property. The order value of a
<label> is changed from
2 when its checkbox is checked, moving it from below the “Not Done”
<h2> to below the “Done”
Unfortunately, keyboard navigation and many screen readers will follow the order of elements in the DOM, even if they’ve been visually reordered using CSS flexbox. This makes the “Done” and “Not Done” headers useless to screen readers, which is why I added
aria-hidden="true" to them — better they be ignored than cause confusion. Besides that, the split list is still fully operable via keyboard and screen readers will still announce the state of an item (checked/unchecked).
If you’re curious about the counts next to “Done” and “Not Done”, those are generated with CSS counters. Check out this article if you want to learn more.
Last demo. This one shows how to highlight a cross section of data that matches a selected criterion.
Here’s the abbreviated HTML. Notice how the
data-teams attribute is a space-separated list of radio button
id attributes. This is how we map characters to teams.
Regarding accessibility, I use empty
alt attributes because the character names are already in the
<h2> tags — no point having each name read twice. Also, since I’m not actually hiding
<img> elements (just shrinking and fading), this makes it easier to get screen readers to skip unhighlighted characters. I only need to hide the
Here’s the CSS that highlights characters when their team is selected.
I know these selectors look hairy, but they’re not so bad. Let’s dissect line 1 as an example. Talking it out, “when the element with an
id of ‘original’ is checked, look within the sibling ‘characters’ element for anything with a
data-teams attribute containing ‘original’, then find the
<h2> within. Repeat for ‘force’, ‘factor’, and ‘hellfire’ on lines 2-4. Now do it all again on lines 8-11, but for
<img> instead of
Update: The kind folks at Webucator put together a video tutorial for this article.