Accordions, the easy way

Accordions, the easy way
Photo by Florencia Viadana / Unsplash

Accordions are a great way to make a lot of informational content organized and not so in-your-face. You can neatly show more information about specific items people want to know more about. Just like how we usually use folders and books to store our real like notes, or how a music store has all the albums categories by genre and band.

I've always felt like it's such a shame having to use so much Javascript to accomplish good accordions, instead of using CSS.

The easiest way to do it with CSS, is to use max-height between a high value and 0 to show and hide the content, but the problem is to know what height you need. Usually people just add a high value, like 999px in there and hope for the best - however, this messes with transition timings etc.

A better way, in my opinion, is to still use max-height and a wee bit of Javascript to just update how tall the content is on page load. We can then save this as a css variable, and use that in our css. We can still use JS to add events to open/close the accordion, or use other techniques like hidden inputs.

Using hidden inputs gives us a good advantage from start as well, we can choose if we want to allow multiple open (checkboxes) or one at a time (radio) with minimal code changes. We'd only need to change the type of the input, and make sure they are grouped as a set of radio buttons as well!

Here's some code to illustrate the technique:

<div class="wrapper">
  <ul class="Accordion">
    <li>
      <input type="checkbox" id="ac1">
      <label for="ac1">Accordion 1</label>
      <div class="content">
        <p>Id diam vel quam elementum pulvinar. Feugiat nibh sed pulvinar proin gravida hendrerit lectus a.</p>
      </div>
    </li>
    <li>
      <input type="checkbox" id="ac2">
      <label for="ac2">Accordion 2</label>
      <div class="content">
        <p>Elit pellentesque habitant morbi tristique senectus et netus. Interdum posuere lorem ipsum dolor sit...</p>
      </div>
    </li>
    <li>
      <input type="checkbox" id="ac3">
      <label for="ac3">Accordion 3</label>
      <div class="content">
        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et...</p>
      </div>
    </li>
    <li>
      <input type="checkbox" id="ac4">
      <label for="ac4">Accordion 4</label>
      <div class="content">
        <p>Viverra ipsum nunc aliquet bibendum enim. Sed elementum tempus egestas sed sed risus pretium quam vulputate. Suscipit tellus mauris a diam maecenas sed enim.</p>
      </div>
    </li>
  </ul>
  <ul class="Accordion">
    <li>
      <!-- For radio buttons, remember to add a name so we switch radio instead of checking more! -->
      <input type="radio" id="acr1" name="acGroup" checked="checked">
      <label for="acr1">Accordion 1</label>
      <div class="content">
        <p>Lorem ipsum dolor sit amet...</p>
      </div>
    </li>
    <li>
      <input type="radio" id="acr2" name="acGroup">
      <label for="acr2">Accordion 2</label>
      <div class="content">
        <p>Viverra ipsum nunc aliquet bibendum enim. Sed elementum tempus egestas sed sed risus pretium quam vulputate. Suscipit tellus mauris a diam maecenas sed enim.</p>
      </div>
    </li>
    <li>
      <input type="radio" id="acr3" name="acGroup">
      <label for="acr3">Accordion 3</label>
      <div class="content">
        <p>Odio tempor orci dapibus ultrices. Id velit ut tortor pretium viverra suspendisse potenti nullam...</p>
      </div>
    </li>
    <li>
      <input type="radio" id="acr4" name="acGroup">
      <label for="acr4">Accordion 4</label>
      <div class="content">
        <p>Id diam vel quam elementum pulvinar. Feugiat nibh sed pulvinar proin gravida hendrerit lectus a.</p>
      </div>
    </li>
  </ul>
</div>

As you can see, we have one set of accordions using checkboxes, and another using radio buttons to illustrate the difference. Let's add some basic CSS to it. This example uses LESS but you can easily convert it to regular CSS or SCSS if you want to.

// Just the wrapper css, no one cares about this.
.wrapper {
  display: flex;
  gap: 20px;
  padding: 20px;
}

// Accordion css. Purposely minimal styling to show the technique instead
.Accordion {
  list-style: none;
  margin: 0;
  padding: 0;
  
  li {
    border-top: 1px solid #000;
    
    &:last-child {
      border-bottom: 1px solid #000;
    }
  }
  
  input { // let's hide the input, it's just used to check if we want the accordion open or not
    height: 0;
    width: 0;
    visibility: hidden;
    opacity: 0;
    position: absolute;
  }
  
  label { // base styling of the accordion toggle
    display: block;
    padding: 8px 16px;
    cursor: pointer;
    position: relative;
    
    // This is to add the handle icon
    &::before, &::after{
      background: #000;
      content: "";
      height: 2px;
      right: 16px;
      position: absolute;
      top: 50%;
      transition: transform .5s ease;
      width: 8px;
    }
    &::before{
      transform: rotate(45deg);
      right: 21px;
    }
    &::after{
      transform: rotate(-45deg);
    }
  }
  
  label + .content { // this is the closed content
    max-height: 0; // Fully collapsed as standard
    transition: max-height .3s ease-in-out;
    overflow: hidden; // We need to hide the overflow!
    box-sizing: border-box;
    
    p {
      margin: 0 16px 16px;
    }
  }
}

Really straight forward. Now, we'll add the open state of them. We will check if the input is checked, and if it is, we know we should have the next content open!

So the only thing we need to add, is this:

  // Change the rotation of the "icon" when opened, which we'll do by just flipping the values between ::before and ::after
  input:checked + label {
    &::before{
      transform: rotate(-45deg);
    }
    &::after{
      transform: rotate(45deg);
    }
  }

  // Tell the content after the input and label to use our CSS variable (that is local to just this content). We'll also provide a fallback to a high number, just in case.
  input:checked + label + .content {
    max-height: var(--accordion-max-height, 999px);
  }

After this, we only need a very small snippet of Javascript, to set the actual height of all content elements on page load. We'll store the values as our CSS variable, scoped to the .content div itself.

const accordionContents = document.querySelectorAll(`.Accordion .content`);
[...accordionContents].forEach(el => {
  el.style.setProperty(`--accordion-max-height`, `${el.scrollHeight}px`);
});

Here's a quick codepen that you can play around with; https://codepen.io/wazp/pen/WNWMrmg

Mastodon