Building a Bootstrap Accordion with the details/summary HTML tags
Contents
I recently had to build an accordion for a small single-page project. As I was using Bootstrap 5 for the CSS I wanted to see if I could apply the accordion styling to the <details>
HTML5 tag without needing to use Bootstrap JavaScript. Here’s the solution I came up with.
Accordion Title 1
Last Friday night, yeah I think we broke the law, always say we're gonna stop. And on my 18th Birthday we got matching tattoos. All to me, give it all to me. Don't be a chicken boy, stop acting like a bitch. Suiting up for my crowning battle. Respect.
Accordion Title 2
Why don't you let me stop by? We freak in my jeep, Snoop Doggy Dogg on the stereo. Got a motel and built a fort out of sheets. It’s in the palm of your hand now baby. I was tryna hit it and quit it. Trying to connect the dots, don't know what to tell my boss.
Accordion Title 3
I know you get me so I let my walls come down, down. A perfect storm, perfect storm. Yes, we make angels cry, raining down on earth from up above. Do you ever feel like a plastic bag. The girl's a freak, she drive a jeep in Laguna Beach.
HTML
<div class="accordion border-bottom-0">
<details class="accordion-item border-bottom-0">
<summary class="accordion-button">
<div class="accordion-header user-select-none">Accordion Title 1</div>
</summary>
<div class="accordion-body border-bottom">
<p>....</p>
</div>
</details>
<details class="accordion-item border-bottom-0">
<summary class="accordion-button">
<div class="accordion-header user-select-none">Accordion Title 2</div>
</summary>
<div class="accordion-body border-bottom">
<p>....</p>
</div>
</details>
<details class="accordion-item">
<summary class="accordion-button">
<div class="accordion-header user-select-none">Accordion Title 3</div>
</summary>
<div class="accordion-body">
<p>....</p>
</div>
</details>
</div>
Custom CSS
To animate the chevron rotating we need to add a few CSS rules.
details.accordion-item:not([open]) .accordion-button {
background-color: var(--bs-accordion-bg);
}
details.accordion-item:not([open]):last-of-type .accordion-button {
border-bottom-right-radius: var(--bs-accordion-border-radius);
border-bottom-left-radius: var(--bs-accordion-border-radius);
}
details.accordion-item:not([open]) .accordion-button::after {
background-image: var(--bs-accordion-btn-active-icon);
transform: unset;
}
details.accordion-item[open] .accordion-button::after {
background-image: var(--bs-accordion-btn-icon);
transform: var(--bs-accordion-btn-icon-transform);
}
Animating the Collapse with the Web Animation API
An optional extra step is to animate the collapse of the accordion using the Web Animation API
. The script is less than 1 KB of JavaScript (minified & gzipped) . The animation is triggered by the click
event on the <summary>
element.
class Accordion {
constructor(i) {
(this.el = i),
(this.summary = i.querySelector("summary")),
(this.content = i.querySelector(".accordion-body")),
(this.animation = null),
(this.isClosing = !1),
(this.isExpanding = !1),
this.summary.addEventListener("click", (i) => this.onClick(i));
}
onClick(i) {
i.preventDefault(),
(this.el.style.overflow = "hidden"),
this.isClosing || !this.el.open
? this.open()
: (this.isExpanding || this.el.open) && this.shrink();
}
shrink() {
this.isClosing = !0;
let i = `${this.el.offsetHeight}px`,
t = `${this.summary.offsetHeight}px`;
this.animation && this.animation.cancel(),
(this.animation = this.el.animate(
{ height: [i, t] },
{ duration: 200, easing: "ease-in-out" }
)),
(this.animation.onfinish = () => this.onAnimationFinish(!1)),
(this.animation.oncancel = () => (this.isClosing = !1));
}
open() {
(this.el.style.height = `${this.el.offsetHeight}px`),
(this.el.open = !0),
window.requestAnimationFrame(() => this.expand());
}
expand() {
this.isExpanding = !0;
let i = `${this.el.offsetHeight}px`,
t = `${this.summary.offsetHeight + this.content.offsetHeight}px`;
this.animation && this.animation.cancel(),
(this.animation = this.el.animate(
{ height: [i, t] },
{ duration: 400, easing: "ease-out" }
)),
(this.animation.onfinish = () => this.onAnimationFinish(!0)),
(this.animation.oncancel = () => (this.isExpanding = !1));
}
onAnimationFinish(i) {
(this.el.open = i),
(this.animation = null),
(this.isClosing = !1),
(this.isExpanding = !1),
(this.el.style.height = this.el.style.overflow = "");
}
}
document.querySelectorAll("details").forEach((i) => {
new Accordion(i);
});