Menu

How to Animate the Details Element Using WAAPI

November 5th, 2020

Animating accordions in JavaScript has been one of the most asked animations on websites. Fun fact: jQuery’s slideDown() function was already available in the first version in 2006.

In this article, we will see how you can animate the native <details> element using the Web Animations API.

HTML setup

First, let’s see how we are gonna structure the markup needed for this animation.

The <details> element needs a <summary> element. The summary is the content visible when the accordion is closed.
All the other elements within the <details> are part of the inner content of the accordion. To make it easier for us to animate that content, we are wrapping it inside a <div>.

<details>
  <summary>Summary of the accordion</summary>
  <div class="content">
    <p>
      Lorem, ipsum dolor sit amet consectetur adipisicing elit.
      Modi unde, ex rem voluptates autem aliquid veniam quis temporibus repudiandae illo, nostrum, pariatur quae!
      At animi modi dignissimos corrupti placeat voluptatum!
    </p>
  </div>
</details>

Accordion class

To make our code more reusable, we should make an Accordion class. By doing this we can call new Accordion() on every <details> element on the page.

class Accordion {
  // The default constructor for each accordion
  constructor() {}

  // Function called when user clicks on the summary
  onClick() {}

  // Function called to close the content with an animation
  shrink() {}

  // Function called to open the element after click
  open() {}

  // Function called to expand the content with an animation
  expand() {}

  // Callback when the shrink or expand animations are done
  onAnimationFinish() {}
}

Constructor()

The constructor is the place we save all the data needed per accordion.

constructor(el) {
  // Store the <details> element
  this.el = el;
  // Store the <summary> element
  this.summary = el.querySelector('summary');
  // Store the <div class="content"> element
  this.content = el.querySelector('.content');

  // Store the animation object (so we can cancel it, if needed)
  this.animation = null;
  // Store if the element is closing
  this.isClosing = false;
  // Store if the element is expanding
  this.isExpanding = false;
  // Detect user clicks on the summary element
  this.summary.addEventListener('click', (e) => this.onClick(e));
}

onClick()

In the onClick() function, you’ll notice we are checking if the element is being animated (closing or expanding). We need to do that in case users click on the accordion while it’s being animated. In case of fast clicks, we don’t want the accordion to jump from being fully open to fully closed.

The <details> element has an attribute, [open], applied to it by the browser when we open the element. We can get the value of that attribute by checking the open property of our element using this.el.open.

onClick(e) {
  // Stop default behaviour from the browser
  e.preventDefault();
  // Add an overflow on the <details> to avoid content overflowing
  this.el.style.overflow = 'hidden';
  // Check if the element is being closed or is already closed
  if (this.isClosing || !this.el.open) {
    this.open();
  // Check if the element is being openned or is already open
  } else if (this.isExpanding || this.el.open) {
    this.shrink();
  }
}

shrink()

This shrink function is using the WAAPI .animate() function. You can read more about it in the MDN docs. WAAPI is very similar to CSS @keyframes. We need to define the start and end keyframes of the animation. In this case, we only need two keyframes, the first one being the current height the element, and the second one is the height of the <details> element once it is closed. The current height is stored in the startHeight variable. The closed height is stored in the endHeight variable and is equal to the height of the <summary>.

shrink() {
  // Set the element as "being closed"
  this.isClosing = true;

  // Store the current height of the element
  const startHeight = `${this.el.offsetHeight}px`;
  // Calculate the height of the summary
  const endHeight = `${this.summary.offsetHeight}px`;

  // If there is already an animation running
  if (this.animation) {
    // Cancel the current animation
    this.animation.cancel();
  }

  // Start a WAAPI animation
  this.animation = this.el.animate({
    // Set the keyframes from the startHeight to endHeight
    height: [startHeight, endHeight]
  }, {
    // If the duration is too slow or fast, you can change it here
    duration: 400,
    // You can also change the ease of the animation
    easing: 'ease-out'
  });

  // When the animation is complete, call onAnimationFinish()
  this.animation.onfinish = () => this.onAnimationFinish(false);
  // If the animation is cancelled, isClosing variable is set to false
  this.animation.oncancel = () => this.isClosing = false;
}

open()

The open function is called when we want to expand the accordion. This function does not control the animation of the accordion yet. First, we calculate the height of the <details> element and we apply this height with inline styles on it. Once it’s done, we can set the open attribute on it to make the content visible but hiding as we have an overflow: hidden and a fixed height on the element. We then wait for the next frame to call the expand function and animate the element.

open() {
  // Apply a fixed height on the element
  this.el.style.height = `${this.el.offsetHeight}px`;
  // Force the [open] attribute on the details element
  this.el.open = true;
  // Wait for the next frame to call the expand function
  window.requestAnimationFrame(() => this.expand());
}

expand()

The expand function is similar to the shrink function, but instead of animating from the current height to the close height, we animate from the element’s height to the end height. That end height is equal to the height of the summary plus the height of the inner content.

expand() {
  // Set the element as "being expanding"
  this.isExpanding = true;
  // Get the current fixed height of the element
  const startHeight = `${this.el.offsetHeight}px`;
  // Calculate the open height of the element (summary height + content height)
  const endHeight = `${this.summary.offsetHeight + this.content.offsetHeight}px`;

  // If there is already an animation running
  if (this.animation) {
    // Cancel the current animation
    this.animation.cancel();
  }

  // Start a WAAPI animation
  this.animation = this.el.animate({
    // Set the keyframes from the startHeight to endHeight
    height: [startHeight, endHeight]
  }, {
    // If the duration is too slow of fast, you can change it here
    duration: 400,
    // You can also change the ease of the animation
    easing: 'ease-out'
  });
  // When the animation is complete, call onAnimationFinish()
  this.animation.onfinish = () => this.onAnimationFinish(true);
  // If the animation is cancelled, isExpanding variable is set to false
  this.animation.oncancel = () => this.isExpanding = false;
}

onAnimationFinish()

This function is called at the end of both the shrinking or expanding animation. As you can see, there is a parameter, [open], that is set to true when the accordion is open, allowing us to set the [open] HTML attribute on the element, as it is no longer handled by the browser.

onAnimationFinish(open) {
  // Set the open attribute based on the parameter
  this.el.open = open;
  // Clear the stored animation
  this.animation = null;
  // Reset isClosing & isExpanding
  this.isClosing = false;
  this.isExpanding = false;
  // Remove the overflow hidden and the fixed height
  this.el.style.height = this.el.style.overflow = '';
}

Setup the accordions

Phew, we are done with the biggest part of the code!

All that’s left is to use our Accordion class for every <details> element in the HTML. To do so, we are using a querySelectorAll on the <details> tag, and we create a new Accordion instance for each one.

document.querySelectorAll('details').forEach((el) => {
  new Accordion(el);
});

Notes

To make the calculations of the closed height and open height, we need to make sure that the <summary> and the content always have the same height.

For example, do not try to add padding on the summary when it’s open because it could lead to jumps during the animation. The same goes for the inner content — it should have a fixed height and we should avoid having content that could change height during the opening animation.

Also, do not add a margin between the summary and the content as it will not be calculated for the heights keyframes. Instead, use a padding directly on the content to add some spacing.

The end

And voilà, we have a nice animated accordion in JavaScript without any library! 🌈

The original post How to Animate the Details Element Using WAAPI.