Skip to main content

Building a Bootstrap Accordion with the details/summary HTML tags

Posted on

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.

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>Accordion body content goes here.</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>Accordion body content goes here.</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>Accordion body content goes here.</p>
    </div>
  </details>
</div>

<style>
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;
}
</style>

The few lines of custom CSS make the chevron rotate when each item opens and closes.

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.

js
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

Christian Oliff profile photo

christianoliff.com


Search

Type to search posts and pages.