How to Create an Accordion with Vanilla JavaScript

I’m back at it with my Let’s Build: With JavaScript series. This week I take a stab at creating a custom accordion component with vanilla JavaScript.

An accordion is a useful component for dealing with a lot of extra content that doesn’t necessarily always need to display. You might use an accordion to call out specific features of an application, show and hide content that isn’t always important but is indeed useful to your given web design. I have even seen an entire F.A.Q. section built successfuly with accordions.

Much like the instrument an accordion shows and had panels when a given panel is active. In this video tutorial I’ll teach you a vanilla JavaScript way of creating a simple accordion. Since we are spending some time crafting the interactions with JavaScript, I also wanted to make the accordion look good as well.

For this I reach for Tailwind CSS. Tailwind is a utility-driven CSS framework, useful for coding designs in a more scalable and resusable fashion. Sure, you can use custom styles or other frameworks just the same but they are rather opinionated approaches in some cases.

Since this tutorial isn’t focused on HTML and CSS I’ll dive right in with code examples.

View the codepen

HTML

My accordion consists of some things about me. My Skills, Links, and more are a decent use case for an accordion.

All of the class names here are mostly from Tailwind. The .accordion-item, .accordion-title-row, and .accordion-content classes I have added as selectors we will make use of with both more custom CSS and our JavaScript.

<div class="container flex flex-col flex-wrap justify-center max-w-md m-auto my-10 fade-in px-2">

  <div class="avatar text-center mb-2">
    <img src="https://s.gravatar.com/avatar/d1f5ca0d7e625f334c5186e112b77ebd?s=80" class="rounded-full border-white border-4 shadow-lg" />
  </div>

  <div class="accordion-item rounded-t overflow-hidden shadow-lg w-full bg-white">
    <div class="accordion-title-row flex justify-between items-center cursor-pointer hover:bg-grey-lighter px-6 py-4">

      <div class="flex">
        <svg viewBox="0 0 20 20" width="20" height="20" class="fill-current text-green mr-2"><title>shield</title><path d="M19 11a7.5 7.5 0 0 1-3.5 5.94L10 20l-5.5-3.06A7.5 7.5 0 0 1 1 11V3c3.38 0 6.5-1.12 9-3 2.5 1.89 5.62 3 9 3v8zm-9 1.08l2.92 2.04-1.03-3.41 2.84-2.15-3.56-.08L10 5.12 8.83 8.48l-3.56.08L8.1 10.7l-1.03 3.4L10 12.09z"></path></svg>
        <h2 class="font-bold text-lg mb-0">Skills</h2>
      </div>

      <div>
        <svg viewBox="0 0 20 20" width="20" height="20" class="fill-current text-grey-dark accordion-arrow"><title>cheveron down</title><path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"></path></svg>
      </div>
    </div>

    <ul class="accordion-content list-reset leading-normal px-8 py-4 bg-grey-lighter hidden">
      <li class="py-2 border-b">CSS</li>
      <li class="py-2 border-b">JavaScript</li>
      <li class="py-2 border-b">Ruby On Rails</li>
      <li class="py-2 border-b">User Experience Design</li>
      <li class="py-2 border-b">Web Design</li>
      <li class="py-2 border-b">Web Development</li>
      <li class="py-2">WordPress</li>
    </ul>
  </div>

  <div class="accordion-item overflow-hidden shadow-lg w-full bg-white">
    <div class="accordion-title-row flex justify-between items-center cursor-pointer hover:bg-grey-lighter px-6 py-4 border-grey-lighter">
      <div class="flex">
        <svg viewBox="0 0 20 20" width="20" height="20" class="fill-current text-blue mr-2"><title>link</title><path d="M9.26 13a2 2 0 0 1 .01-2.01A3 3 0 0 0 9 5H5a3 3 0 0 0 0 6h.08a6.06 6.06 0 0 0 0 2H5A5 5 0 0 1 5 3h4a5 5 0 0 1 .26 10zm1.48-6a2 2 0 0 1-.01 2.01A3 3 0 0 0 11 15h4a3 3 0 0 0 0-6h-.08a6.06 6.06 0 0 0 0-2H15a5 5 0 0 1 0 10h-4a5 5 0 0 1-.26-10z"></path></svg>
        <h2 class="font-bold text-lg">Links</h2>
      </div>

      <div>
        <svg viewBox="0 0 20 20" width="20" height="20" class="fill-current text-grey-dark accordion-arrow"><title>cheveron down</title><path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"></path></svg>
      </div>
    </div>
    <ul class="accordion-content list-reset leading-normal px-8 py-4 bg-grey-lighter hidden">
      <li class="py-2 border-b">
        <a class="no-underline text-blue-dark" href="https://justalever.com">Website</a>
      </li>
      <li class="py-2 border-b">
        <a class="no-underline text-blue-dark" href="https://web-crunch.com">Blog</a>
      </li>
      <li class="py-2 border-b">
        <a class="no-underline text-blue-dark" href="https://youtube.com/c/webcrunch">YouTube</a>

      </li>
      <li class="py-2">
        <a class="no-underline text-blue-dark" href="https://medium.com/the-web-crunch-publication">Medium</a>
      </li>
    </ul>
  </div>

  <div class="accordion-item overflow-hidden shadow-lg w-full bg-white rounded-b">

    <div class="accordion-title-row flex justify-between items-center cursor-pointer hover:bg-grey-lighter px-6 py-4 border-grey-lighter border-t">
      <div class="flex">
        <svg viewBox="0 0 20 20" width="20" height="20" class="fill-current text-orange-dark mr-2"><title>trophy</title><path d="M15 9a3 3 0 0 0 3-3h2a5 5 0 0 1-5.1 5 5 5 0 0 1-3.9 3.9V17l5 2v1H4v-1l5-2v-2.1A5 5 0 0 1 5.1 11H5a5 5 0 0 1-5-5h2a3 3 0 0 0 3 3V4H2v2H0V2h5V0h10v2h5v4h-2V4h-3v5z"></path></svg>
        <h2 class="font-bold text-lg">Stuff I've Done</h2>
      </div>

      <div>
        <svg viewBox="0 0 20 20" width="20" height="20" class="fill-current text-grey-dark accordion-arrow"><title>cheveron down</title><path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"></path></svg>
      </div>
    </div>
    <ul class="accordion-content list-reset leading-normal px-8 py-4 bg-grey-lighter hidden">
      <li class="py-2 border-b">
        <a class="no-underline text-blue-dark hover:underline" href="https://web-crunch.com/books/luxd">LUXD: Learn User Experience Design</a></li>
      <li class="py-2 border-b">
        <a class="no-underline text-blue-dark hover:underline" href="https://web-crunch.com/books/ptt">Pro Tumblr Theming</a>
      </li>
      <li class="py-2">
        <a class="no-underline text-blue-dark hover:underline" href="https://affinicasts.com">Affinicasts</a>
      </li>
    </ul>
  </div>
</div>

Custom Styles

To add some flair I referenced a fun site called heropatterns.com. There you can configure, download, and copy the CSS you need for repeatable background patterns. When copying code you’ll notice the background image itself is a base64 image as opposed to a direct link to a file hosted somewhere.

Besides the custom asthetics, I also defined some styles for when an active class is applied to the .accordion-item div once we manipulate with JavaScript coming up. I want to add an active class to be able to target items within the .accordion-item when it has an additional class of active.

body {
background-color: #51c9ff;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 304 304' width='304' height='304'%3E%3Cpath fill='%231a8eae' fill-opacity='0.19' d='M44.1 224a5 5 0 1 1 0 2H0v-2h44.1zm160 48a5 5 0 1 1 0 2H82v-2h122.1zm57.8-46a5 5 0 1 1 0-2H304v2h-42.1zm0 16a5 5 0 1 1 0-2H304v2h-42.1zm6.2-114a5 5 0 1 1 0 2h-86.2a5 5 0 1 1 0-2h86.2zm-256-48a5 5 0 1 1 0 2H0v-2h12.1zm185.8 34a5 5 0 1 1 0-2h86.2a5 5 0 1 1 0 2h-86.2zM258 12.1a5 5 0 1 1-2 0V0h2v12.1zm-64 208a5 5 0 1 1-2 0v-54.2a5 5 0 1 1 2 0v54.2zm48-198.2V80h62v2h-64V21.9a5 5 0 1 1 2 0zm16 16V64h46v2h-48V37.9a5 5 0 1 1 2 0zm-128 96V208h16v12.1a5 5 0 1 1-2 0V210h-16v-76.1a5 5 0 1 1 2 0zm-5.9-21.9a5 5 0 1 1 0 2H114v48H85.9a5 5 0 1 1 0-2H112v-48h12.1zm-6.2 130a5 5 0 1 1 0-2H176v-74.1a5 5 0 1 1 2 0V242h-60.1zm-16-64a5 5 0 1 1 0-2H114v48h10.1a5 5 0 1 1 0 2H112v-48h-10.1zM66 284.1a5 5 0 1 1-2 0V274H50v30h-2v-32h18v12.1zM236.1 176a5 5 0 1 1 0 2H226v94h48v32h-2v-30h-48v-98h12.1zm25.8-30a5 5 0 1 1 0-2H274v44.1a5 5 0 1 1-2 0V146h-10.1zm-64 96a5 5 0 1 1 0-2H208v-80h16v-14h-42.1a5 5 0 1 1 0-2H226v18h-16v80h-12.1zm86.2-210a5 5 0 1 1 0 2H272V0h2v32h10.1zM98 101.9V146H53.9a5 5 0 1 1 0-2H96v-42.1a5 5 0 1 1 2 0zM53.9 34a5 5 0 1 1 0-2H80V0h2v34H53.9zm60.1 3.9V66H82v64H69.9a5 5 0 1 1 0-2H80V64h32V37.9a5 5 0 1 1 2 0zM101.9 82a5 5 0 1 1 0-2H128V37.9a5 5 0 1 1 2 0V82h-28.1zm16-64a5 5 0 1 1 0-2H146v44.1a5 5 0 1 1-2 0V18h-26.1zm102.2 270a5 5 0 1 1 0 2H98v14h-2v-16h124.1zM242 149.9V160h16v34h-16v62h48v48h-2v-46h-48v-66h16v-30h-16v-12.1a5 5 0 1 1 2 0zM53.9 18a5 5 0 1 1 0-2H64V2H48V0h18v18H53.9zm112 32a5 5 0 1 1 0-2H192V0h50v2h-48v48h-28.1zm-48-48a5 5 0 0 1-9.8-2h2.07a3 3 0 1 0 5.66 0H178v34h-18V21.9a5 5 0 1 1 2 0V32h14V2h-58.1zm0 96a5 5 0 1 1 0-2H137l32-32h39V21.9a5 5 0 1 1 2 0V66h-40.17l-32 32H117.9zm28.1 90.1a5 5 0 1 1-2 0v-76.51L175.59 80H224V21.9a5 5 0 1 1 2 0V82h-49.59L146 112.41v75.69zm16 32a5 5 0 1 1-2 0v-99.51L184.59 96H300.1a5 5 0 0 1 3.9-3.9v2.07a3 3 0 0 0 0 5.66v2.07a5 5 0 0 1-3.9-3.9H185.41L162 121.41v98.69zm-144-64a5 5 0 1 1-2 0v-3.51l48-48V48h32V0h2v50H66v55.41l-48 48v2.69zM50 53.9v43.51l-48 48V208h26.1a5 5 0 1 1 0 2H0v-65.41l48-48V53.9a5 5 0 1 1 2 0zm-16 16V89.41l-34 34v-2.82l32-32V69.9a5 5 0 1 1 2 0zM12.1 32a5 5 0 1 1 0 2H9.41L0 43.41V40.6L8.59 32h3.51zm265.8 18a5 5 0 1 1 0-2h18.69l7.41-7.41v2.82L297.41 50H277.9zm-16 160a5 5 0 1 1 0-2H288v-71.41l16-16v2.82l-14 14V210h-28.1zm-208 32a5 5 0 1 1 0-2H64v-22.59L40.59 194H21.9a5 5 0 1 1 0-2H41.41L66 216.59V242H53.9zm150.2 14a5 5 0 1 1 0 2H96v-56.6L56.6 162H37.9a5 5 0 1 1 0-2h19.5L98 200.6V256h106.1zm-150.2 2a5 5 0 1 1 0-2H80v-46.59L48.59 178H21.9a5 5 0 1 1 0-2H49.41L82 208.59V258H53.9zM34 39.8v1.61L9.41 66H0v-2h8.59L32 40.59V0h2v39.8zM2 300.1a5 5 0 0 1 3.9 3.9H3.83A3 3 0 0 0 0 302.17V256h18v48h-2v-46H2v42.1zM34 241v63h-2v-62H0v-2h34v1zM17 18H0v-2h16V0h2v18h-1zm273-2h14v2h-16V0h2v16zm-32 273v15h-2v-14h-14v14h-2v-16h18v1zM0 92.1A5.02 5.02 0 0 1 6 97a5 5 0 0 1-6 4.9v-2.07a3 3 0 1 0 0-5.66V92.1zM80 272h2v32h-2v-32zm37.9 32h-2.07a3 3 0 0 0-5.66 0h-2.07a5 5 0 0 1 9.8 0zM5.9 0A5.02 5.02 0 0 1 0 5.9V3.83A3 3 0 0 0 3.83 0H5.9zm294.2 0h2.07A3 3 0 0 0 304 3.83V5.9a5 5 0 0 1-3.9-5.9zm3.9 300.1v2.07a3 3 0 0 0-1.83 1.83h-2.07a5 5 0 0 1 3.9-3.9zM97 100a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-48 32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm32 48a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm32-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm32 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16-64a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 96a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-144a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-96 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm96 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16-64a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-32 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM49 36a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-32 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm32 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM33 68a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-48a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 240a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16-64a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16-32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm80-176a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm32 48a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm112 176a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM17 180a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM17 84a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm32 64a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6z'%3E%3C/path%3E%3C/svg%3E");
}

.accordion-item {

  &.active {

    .accordion-title-row .accordion-arrow {
      transform: rotate(-180deg);
      transition: ease .3s transform;
    }

    .accordion-content {
      transition: ease .3s all;
      animation-name: fadeIn;
      animation-duration: .6s;
      animation-fill-mode: both;
      will-change: transform;
    }
  }
}

.fade-in {
  animation-name: fadeIn;
  animation-duration: .6s;
  animation-fill-mode: both;
}

@keyframes fadeIn {
  from {
    opacity: 0;
  }

  to {
    opacity: 1;
  }
}

The JavaScript

Finally on to the JavaScript. There’s not a lot of code needed to get the job done. We’ll need to iterate through all the accordion items as well as their nested .accordion-content uls. We’ll listen for a click event on each .accordion-items .accordion-title-row. For even more details be sure to follow along in the video portion of this tutorial.

function addClass(el, klass) {
  el.classList.add(klass);
}

function removeClass(el, klass) {
  el.classList.remove(klass);
}

const accordionItems = document.querySelectorAll('.accordion-item');
const accordionContentPanes = document.querySelectorAll('.accordion-content');

// Show first by default
accordionItems[0].querySelector('.accordion-content').classList.remove('hidden');

// Hide each besides the targeted accordion on click
accordionItems.forEach(function(accordion) {
  // Clicked accordeions clickable target
  const accordionTitleRow = accordion.firstElementChild;

  accordionTitleRow.addEventListener('click', toggleAccordion);
  // console.log(accordion.firstElementChild);  
});

function toggleAccordion(e) {
  accordionContentPanes.forEach(function(content) {
    // Check if clicked row matches the content's previous element sibling
    console.log(content); 
    if (content.previousElementSibling === e.target) {
      removeClass(content, 'hidden'); 
      addClass(content.parentElement, 'active');
    } else {
      removeClass(content.parentElement, 'active');
      addClass(content, 'hidden');
    }
  });
}

Thanks so much for watching/reading. There’s much more to come. In case you’re new here be sure to check out the other videos in this series listed below:

Let’s Build: With JavaScript Series