Multi-Level Accordion Menu

Multi-Level Accordion Menu

A simple CSS accordion menu with support for sub level items.

👉 Important: this experiment is built upon the CodyHouse Framework.

Today's resource is a handy accordion menu with support for groups/subitems. It works with CSS only, using the :checked pseudo-class selector on the checkboxes input elements. However we included a version with JavaScript as well, in case you prefer a subtle animation compared to the instant default effect. Your call!

The first user case I can think of for this resources is a "layer organizer". Think of Sublime Text sidebar, or Photoshop layer window. Anyway, I'm sure you'll find a use for this new snippet to store in your arsenal ;)

Creating the structure

The HTML structure is pretty simple: the accordion is an unordered list. If a list item contains subitems, then we insert an input[type=checkbox] and its label. Also, we add the .cd-accordion__item--has-children class to the list item. All "standard" list items contain just an anchor tag.

<ul class="cd-accordion margin-top--lg margin-bottom--lg">
  <li class="cd-accordion__item cd-accordion__item--has-children">
    <input class="cd-accordion__input" type="checkbox" name ="group-1" id="group-1">
    <label class="cd-accordion__label cd-accordion__label--icon-folder" for="group-1"><span>Group 1</span></label>

    <ul class="cd-accordion__sub cd-accordion__sub--l1">
      <li class="cd-accordion__item cd-accordion__item--has-children">
        <input class="cd-accordion__input" type="checkbox" name ="sub-group-1" id="sub-group-1">
        <label class="cd-accordion__label cd-accordion__label--icon-folder" for="sub-group-1"><span>Sub Group 1</span></label>

        <ul class="cd-accordion__sub cd-accordion__sub--l2">
          <li class="cd-accordion__item"><a class="cd-accordion__label cd-accordion__label--icon-img" href="#0"><span>Image</span></a></li>
          <li class="cd-accordion__item"><a class="cd-accordion__label cd-accordion__label--icon-img" href="#0"><span>Image</span></a></li>
          <li class="cd-accordion__item"><a class="cd-accordion__label cd-accordion__label--icon-img" href="#0"><span>Image</span></a></li>
        </ul>
      </li>
  
      <li class="cd-accordion__item"><a class="cd-accordion__label cd-accordion__label--icon-img" href="#0"><span>Image</span></a></li>
      <li class="cd-accordion__item"><a class="cd-accordion__label cd-accordion__label--icon-img" href="#0"><span>Image</span></a></li>
    </ul>
  </li>

  <li class="cd-accordion__item cd-accordion__item--has-children">
    <a class="cd-accordion__label cd-accordion__label--icon-img" href="#0"><span>Image</span></a>
  </li>
  <li class="cd-accordion__item cd-accordion__item--has-children">
    <a class="cd-accordion__label cd-accordion__label--icon-img" href="#0"><span>Image</span></a>
  </li>
</ul> <!-- cd-accordion -->

Adding style

We use a smart (and quite standard nowadays) technique to detect the click and show sub content with CSS only: by including a checkbox input element, we can use the :checked pseudo-class and the general sibling selector (div ~ div) to change the display mode of the sub <ul> element from "none" to "block".

Step by step: first of all, we have to make sure that the checkbox input element covers the entire list item that contains subitems. Put in other words: we need to create a custom checkbox.

So, firstly, you need to make sure that when you click on the label, the checkbox is checked/unchecked as well. This is achieved by using the "for" attribute inside the label (label "for" attribute = input "name" and "id" attributes. See html section above). This way you can simply hide the input element and work with the label instead.

.cd-accordion__input { // hide native checkbox
  position: absolute;
  opacity: 0;
}

.cd-accordion__label {
  position: relative;
  display: flex;
  align-items: center;
  padding: var(--space-sm) var(--space-md);
  background: var(--cd-color-1);
  box-shadow: inset 0 -1px lightness(var(--cd-color-1), 1.2);
  color: var(--color-white);
}

Now notice in the HTML structure that input, label and the unordered list (that we make visible on click) are siblings. By using the :checked pseudo-class, you can set the following process in motion: when the checkbox input is checked (click on label), take the .cd-accordion__sub sibling element and change its display value from "none" to "block":

.cd-accordion__sub {
  display: none; // by default hide all sub menus
}

.cd-accordion__input:checked ~ .cd-accordion__sub { // show children when item is checked
  display: block;
}

If you want to gently animate the opening phase, make sure to add the .cd-accordion--animated class to the main .cd-accordion element.

💌 Join our newsletter

Get our monthly recap with the latest CodyHouse news