In this short tutorial, we’ll learn how to build a CSS-only flexible accordion component by taking advantage of the “CSS checkbox hack technique”. Most importantly, our component will be fully responsive and its layout will switch between horizontal and vertical depending on the viewport size.
Along the way, we’ll discuss how the CSS Checkbox Hack works, and look at some other Checkbox Hack inspiration from developers on CodePen. Sound interesting?
Our Responsive CSS Accordion
Here’s what we’ll be building during this tutorial:
Note: This tutorial assumes some flexbox knowledge. If you’re just beginning, check out this beginners flexbox tutorial:
Wait, What’s the CSS Checkbox Hack?
The CSS checkbox hack allows you to control certain styles depending on whether checkboxes (or radio buttons) are checked or not. It uses the :checked
pseudo-class selector, which enables us to say “if a checkbox is checked, apply these style rules to its sibling etc.”
Developers usually hide the input itself, controlling the checked value via its label, so users can’t even tell they’re toggling a checkbox at all.
It’s a favourite of mine; in fact, I’ve used the same technique in several tutorials:
-
CSS
Quick Tip: How to Create an Off-Canvas Feedback Form With Pure CSS
George Martsoukos
-
CSS Selectors
How to Build a Filtering Component in Pure CSS
George Martsoukos
1. Begin With the HTML Markup
For the purposes of this exercise, we’ll grab some content from Wikipedia about four different things: animals, plants, space, and rivers.
Then, we’ll create the corresponding radio buttons which we’ll group under the wiki
keyword:
<input type="radio" id="animal" name="wiki" value="Animal" checked> <input type="radio" id="plant" name="wiki" value="Plant"> <input type="radio" id="space" name="wiki" value="Space"> <input type="radio" id="river" name="wiki" value="River">
Build an Unordered List
Next up, we’ll specify an unordered list with four list items. Each list item will represent an accordion item/pane and hold two elements:
- Firstly, a label which will serve as the accordion’s title and be responsible for opening the target item. Its
for
value should match theid
value of one of the aforementioned radio buttons. - Secondly, a
div
element which will serve as the accordion’s content area.
By default, one pane in our accordion must be open. With that in mind, let’s add the checked
attribute to the first radio button.
Putting it all together, here’s the markup that we’ll need:
<ul class="accordion"> <li> <label for="animal" class="accordion-title"> <span>...</span> <span class="accordion-heading">...</span> </label> <div class="accordion-content">...</div> </li> <li> <label for="plant" class="accordion-title"> <span>...</span> <span class="accordion-heading">...</span> </label> <div class="accordion-content">...</div> </li> <li> <label for="space" class="accordion-title"> <span>...</span> <span class="accordion-heading">...</span> </label> <div class="accordion-content">...</div> </li> <li> <label for="river" class="accordion-title"> <span>...</span> <span class="accordion-heading">...</span> </label> <div class="accordion-content">...</div> </li> </ul>
2. Define the Styles
With the markup ready (and inline with the CSS checkbox hack I described earlier) we’ll first visually hide the radio buttons by moving them off screen:
input[type="radio"] { position: absolute; left: -9999px; }
The accordion will have a maximum width, a minimum height, and behave as a flex container:
/*CUSTOM VARIABLES HERE*/ .accordion { display: flex; width: calc(100% - 20px); max-width: 800px; min-height: 380px; margin: 0 auto; background: var(--accordion-color); color: var(--white); }
Also, each list item will serve as a flex wrapper:
.accordion li { display: flex; }
The accordion items should be separated, so let’s give them a border:
/*CUSTOM VARIABLES HERE*/ .accordion li:not(:last-child) { border: 1px solid var(--separator-color); }
Each title (label) inside an item will be a flex container and its child elements will be distributed vertically across the main axis. In addition, all items will have a 70px width:
/*CUSTOM VARIABLES HERE*/ .accordion .accordion-title { display: flex; flex-direction: column; justify-content: space-between; width: 70px; font-size: 1.4rem; font-weight: bold; line-height: normal; padding: 20px 10px; background: var(--title-color); transition: color 0.1s; } .accordion .accordion-title:hover { color: var(--active-color); }
The text inside the .accordion-heading
will be rotated vertically:
.accordion .accordion-heading { display: inline-block; white-space: nowrap; transform-origin: bottom; transform: rotate(-90deg) translate(50%, 50%); }
Initially, apart from the first pane, all the other panes will be hidden:
.accordion .accordion-content { display: none; align-items: center; padding: 20px; }
3. Checkbox Hack: Toggle the Panes
Now for the Magic. Each time we click on a label, its associated content should appear. To make that happen, we’ll take advantage of the :checked
pseudo-class, the subsequent-sibling selector (~
), and the adjacent sibling combinator (+
). If you need a refresher of what these selectors do, check out this tutorial:
So, when an item becomes visible, it will receive display: flex
and not display: block
. This is because we want to vertically center its content by taking advantage of the align-items: center
property value. Additionally, the colors of the active pane have to change, so a visitor can clearly understand which pane is open.
Optionally, each time a radio button receives focus, we can add an outline to its associated label. This small detail will help us enhance the accessibility of our component.
Here’s the related CSS stuff:
/*CUSTOM VARIABLES HERE*/ [value="Animal"]:checked ~ .accordion [for="animal"] + .accordion-content, [value="Plant"]:checked ~ .accordion [for="plant"] + .accordion-content, [value="Space"]:checked ~ .accordion [for="space"] + .accordion-content, [value="River"]:checked ~ .accordion [for="river"] + .accordion-content { display: flex; } [value="Animal"]:checked ~ .accordion [for="animal"], [value="Plant"]:checked ~ .accordion [for="plant"], [value="Space"]:checked ~ .accordion [for="space"], [value="River"]:checked ~ .accordion [for="river"] { color: var(--active-color); } /*optional*/ [value="Animal"]:focus ~ .accordion [for="animal"], [value="Plant"]:focus ~ .accordion [for="plant"], [value="Space"]:focus ~ .accordion [for="space"], [value="River"]:focus ~ .accordion [for="river"] { outline: 1px solid var(--active-color); }
4. Going Responsive
As we’ve already discussed, on small screens the items inside the accordion should be stacked, so the accordion will have a vertical layout. Thanks to flexbox, we can implement this design without putting in too much effort. In fact, all we have to do is to update the direction of the flex wrappers and reset the transform
property value of the .accordion-heading
.
Have a look at the responsive styles within a media query below:
@media screen and (max-width: 650px) { .accordion { min-height: 0; } .accordion, .accordion li { flex-direction: column; } .accordion .accordion-title { flex-direction: row; width: auto; } .accordion .accordion-heading { transform: none; } .accordion .accordion-title, .accordion .accordion-content { padding: 20px; } }
5. Bonus: Limited Content
In our case, there’s a lot of content inside each of the accordion items, so everything looks great. But, let’s ensure that the accordion will still work fine when there isn’t enough content within the panes (e.g. a pane contains only social links).
To satisfy this scenario, we have to do two things:
- Add
flex-grow: 1
to the list item which contains the active pane. To target only that item and not all of them, we’ll need to add a new custom attribute (data-radio
) to the list items with value their label’s value. - Add
flex-grow: 1
to the.accordion-content
, so it will expand and cover the full parent width. - Center horizontally the content of the
.accordion-content
thanks to thejustify-content: center
.
With all the above in mind, we’ll modify our HTML as follows:
<ul class="accordion"> <li data-radio="animal">...</li> <li data-radio="plant">...</li> <li data-radio="space">...</li> <li data-radio="river">...</li> </ul>
Then, in the CSS we’ll include these styles:
.accordion .accordion-content { justify-content: center; flex-grow: 1; } [value="Animal"]:checked ~ .accordion [data-radio="animal"], [value="Plant"]:checked ~ .accordion [data-radio="plant"], [value="Space"]:checked ~ .accordion [data-radio="space"], [value="River"]:checked ~ .accordion [data-radio="river"] { flex-grow: 1; }
Conclusion
That’s it folks! In this quick tutorial, we managed to build a pure CSS accordion by taking advantage of the “CSS checkbox hack technique”. Hopefully, you enjoyed this exercise and you’ll spend some time extending it for your own specific purposes.
Here’s a reminder of what we built:
Extra Project
To conclude, just keep in mind that with this implementation only one pane of the accordion can be open at a time. That’s because we used radio buttons in the markup. In case you want to display multiple accordion items at the same time, replace the radio buttons with checkboxes and make the necessary changes (hide the checkboxes) in the CSS.
As always, thanks a lot for reading!
Checkbox Hack Inspiration on CodePen
Where better to see what others have built with the CSS Checkbox Hack than CodePen? Here are some great examples to whet your appetite:
Responsive Emoji Toggles by George W. Park
CSS Only Tabbed Content by Stephen Greig
Modal pure CSS – “Checkbox Hack” by BeardedBear
CSS checkbox hack nav by JAD3
Further Reading
Check out these resources to learn more about similar CSS techniques, and other ways of building accordion components (did someone say Bootstrap?!):