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 a padding on the summary when it’s open because it could lead to jumps during the animation. 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! ?
Source: CSS-tricks.com