DEV Community

Vadim Filimonov
Vadim Filimonov

Posted on

Accordion in vanilla JavaScript

Let's figure out how to make an accordion in vanilla JavaScript.

This article describes an accordion that can be fully animated.

HTML

<dl>
  <dt class="question js-accordion">
    <button class="question__trigger" type="button">
      First Question?
    </button>
  </dt>
  <dd class="answer">
    <div>
      Lorem ipsum dolor sit amet consectetur adipisicing elit. Id, itaque quisquam? Quia cum alias in, beatae soluta dicta fuga corrupti magni? Alias minus nostrum qui at corporis, magni optio ipsam!
    </div>
  </dd>
  <dt class="question js-accordion">
    <button class="question__trigger" type="button">
      Second Question?
    </button>
  </dt>
  <dd class="answer">
    <div class="answer__content">
      Lorem ipsum dolor sit amet consectetur adipisicing elit. Id, itaque quisquam? Quia cum alias in, beatae soluta dicta fuga corrupti magni? Alias minus nostrum qui at corporis, magni optio ipsam!
    </div>
  </dd>
</dl>
Enter fullscreen mode Exit fullscreen mode

What immediately catches your eye are the dl, dt, and dd tags. Why can't you just use div everywhere?
In principle, you can, but accordion is a good example of a list of terms and definitions, which is what these tags are for.

Let's break it down piece by piece:

  <dt class="question js-accordion">
    <button class="question__trigger" type="button">
      First Question?
    </button>
  </dt>
Enter fullscreen mode Exit fullscreen mode

We put a question in dt, but why wrap it in a button? Again semantics: when a user clicks on an element - something has to happen, a button has to be used for that.
The second reason is keyboard accessibility: the accordion can be accessed via tab and expanded via the space bar.

  <dd class="answer">
    <div>
      Lorem ipsum dolor sit amet consectetur adipisicing elit. Id, itaque quisquam? Quia cum alias in, beatae soluta dicta fuga corrupti magni? Alias minus nostrum qui at corporis, magni optio ipsam!
    </div>
  </dd>
Enter fullscreen mode Exit fullscreen mode

In dd we put the answer wrapped in an additional div?
The point is that in order to animate the "unfolding" of the accordion, you need to know the height of the answer.

.answer {
  overflow: hidden;
  height: 0;
  transition: height 0.5s;
}
Enter fullscreen mode Exit fullscreen mode

In the inactive state, the answer tag has zero height, so we put an additional div into it, the size of which we will measure.

As soon as the accordion is expanded, we will assign the previously measured height to answer.

JavaScript

Find all the accordions on the page and initialize them:

const elements = [...document.querySelectorAll('.js-accordion')];
elements.forEach(accordion);
Enter fullscreen mode Exit fullscreen mode

Let's use the closure:

function accordion(element) {
  // object, in which we will store all the necessary information
  const instance = {};

  function init() {
    // find question and answer
    findElements(instance, element);
    // calc height of answer
    measureHeight(instance);
    // let's add the logic of pressing the button
    subscribe(instance);
  }

  init();
}
Enter fullscreen mode Exit fullscreen mode

findElements

function findElements(object, element) {
  const instance = object;
  // element - is the "question" that is clicked on
  instance.element = element;
  // target - is the "answer" to be "expanded"
  instance.target = element.nextElementSibling;
}
Enter fullscreen mode Exit fullscreen mode

If you remember the markup - the answer always follows the question, that's why we use the nextElementSibling.

measureHeight

function measureHeight(object) {
  const instance = object;
  // calculate the answer height
  instance.height = object.target.firstElementChild.clientHeight;
}
Enter fullscreen mode Exit fullscreen mode

While the target has zero height, its descendant has the same size. This is the size we save.

The answer has a height of 0, but the answer__content does not

subscribe

function subscribe(instance) {
  instance.element.addEventListener('click', (event) => {
    // cancel "default action"
    event.preventDefault();
    // change the state of the accordion
    changeElementStatus(instance);
  });
  // if the window size changes - measure the response height again
  window.addEventListener('resize', () => measureHeight(instance));
}
Enter fullscreen mode Exit fullscreen mode

changeElementStatus

function changeElementStatus(instance) {
  if (instance.isActive) {
    hideElement(instance);
  } else {
    showElement(instance);
  }
}
Enter fullscreen mode Exit fullscreen mode

If the accordion is active, we minimize it, otherwise we unfold it.

hideElement и showElement

function hideElement(object) {
  const instance = object;
  const { target } = instance;
  // reset answer height
  target.style.height = null;
  // make the status inactive
  instance.isActive = false;
}

function showElement(object) {
  const instance = object;
  const { target, height } = instance;
  // set the answer to the height saved in measureHeight
  target.style.height = ${height}px;
  // make the status active
  instance.isActive = true;
}
Enter fullscreen mode Exit fullscreen mode

Image description

As always, a demo at codepen.

Discussion (0)