Good day, friends!
In this article, I'll show you how you can animate the native details element using the Web Animations API .
Let's start with the markup.
The "details" element must contain a "summary" element. summary is the visible part of the content when the accordion is closed.
Any other elements are part of the inner content of the accordion. To make our task easier, we will wrap this content in a div with the class "content".
<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
We need an Accordion class to be able to reuse our code. With such a class, we can instantiate any number of details on the page.
class Accordion {
constructor() {}
// , summary
onClick() {}
// ,
shrink() {}
// ,
open() {}
// ,
expand() {}
// , shrink expand
onAnimationFinish() {}
}
constructor ()
The constructor is used to store the data necessary for the accordion.
constructor(el) {
// details
this.el = el
// summary
this.summary = el.querySelector('summary')
// div "content"
this.content = el.querySelector('.content')
// ( )
this.animation = null
// ?
this.isClosing = false
// ?
this.isExpanding = false
// summary
this.summary.addEventListener('click', (e) => this.onClick(e))
}
onClick ()
In the "onClick" function, we check if the element is in the process of animating (closing or expanding). We need to do this for the case when the user clicks on the accordion before the animation ends. We don't want the accordion to jump from fully open to fully closed.
The "details" element has an "open" attribute added by the browser when the element is opened. We can get the value of this attribute via this.el.open.
onClick(e) {
//
e.preventDefault()
// details "overflow" "hidden"
this.el.style.overflow = 'hidden'
// ,
if (this.isClosing || !this.el.open) {
this.open()
// ,
} else if (this.isExpanding || this.el.open) {
this.shrink()
}
}
shrink ()
The shrink function uses the WAAPI "animate" function. You can read about this feature here . WAAPI is very similar to the CSS "keyframes" statement in that we need to define keyframes for the animation. In this case, we only need two such frames: the first is the current height of the details element (open), the second is the height of the closed details (summary height).
shrink() {
//
this.isClosing = true
//
const startHeight = `${this.el.offsetHeight}px`
// summary
const endHeight = `${this.summary.offsetHeight}px`
//
if (this.animation) {
//
this.animation.cancel()
}
// WAAPI
this.animation = this.el.animate({
//
height: [startHeight, endHeight]
}, {
// , (duration - )
duration: 400,
// (easing (animation-timing-function) - )
easing: 'ease-out'
})
// onAnimationFinish()
this.animation.onfinish = () => this.onAnimationFinish(false)
// , "isClosing" "false"
this.animation.oncancel = () => this.isClosing = false
}
open ()
The "open" function is called when we want to open the accordion. This function does not control the animation of the accordion. First, we calculate the height of the "details" element and add the appropriate inline styles to it. After this is done, we can add an "open" attribute to it in order to make the content visible, but at the same time hidden thanks to overflow: hidden and the fixed height of the element. Next, we wait for the next frame to call the expand function and animate the element.
open() {
//
this.el.style.height = `${this.el.offsetHeight}px`
// details "open"
this.el.open = true
// "expand"
requestAnimationFrame(() => this.expand())
}
expand ()
The expand function is similar to the shrink function, but instead of animating from the element's current height to its closed height, we animate from the element's height to its full height. The total height is the summary height plus the height of the inner content.
expand() {
//
this.isExpanding = true
//
const startHeight = `${this.el.offsetHeight}px`
// ( summary + )
const endHeight = `${this.summary.offsetHeight + this.content.offsetHeight}px`
//
if (this.animation) {
//
this.animation.cancel()
}
// WAAPI
this.animation = this.el.animate({
height: [startHeight, endHeight]
}, {
duration: 400,
easing: 'ease-out'
})
this.animation.onfinish = () => this.onAnimationFinish(true)
this.animation.oncancel = () => this.isClosing = false
}
onAnimationFinish ()
This function is called at the end of the opening and closing animation details. It takes one parameter, a boolean value for the "open" attribute, which is no longer processed by the browser (if you remember, we canceled the default browser behavior in the "onClick" function).
onAnimationFinish(open) {
// "open"
this.el.open = open
// ,
this.animation = null
//
this.isClosing = false
this.isExpanding = false
// overflow
this.el.style.height = this.el.style.overflow = ''
}
Initializing accordions
Fuh! We're almost done.
All that's left to do is create an instance of the Accordion class for each details element on the page.
document.querySelectorAll('details').forEach(el => {
new Accordion(el)
})
Remarks
To correctly calculate the height of an element in the open and closed states, the summary and content must have the same height throughout the animation.
Do not add internal blanks for the open summary, as this can lead to sudden jumps. The same is true for internal content - it must have a fixed height, and you should avoid changing its height while opening details.
Also, do not add external blanks between summary and content, as they will not be taken into account when calculating the height in keyframes. Instead, use padding on your content to add some space.
Conclusion
That's how, easily and simply, we managed to create an accordion in pure JavaScript.
I hope you found something interesting for yourself. Thank you for attention.