Building a Bootstrap Accordion with the details/summary HTML tags

Posted on
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 rounded-top">
      <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);
}

/* Hide the default disclosure triangle on Safari */
summary.accordion-button::-webkit-details-marker {
  display: none;
}

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);
});

You might also like