Stylish Accordion UI Using Details And Summary Elements

Category: Accordion , Javascript | May 16, 2024
Author:jkantner
Views Total:22 views
Official Page:Go to website
Last Update:May 16, 2024
License:MIT

Preview:

Stylish Accordion UI Using Details And Summary Elements

Description:

This is a stylish, responsive, customizable accordion UI built using JavaScript, CSS, and HTML <details> & <summary> elements.

It’s perfect for creating accordion menus and FAQ sections. Users can easily navigate through the accordion by clicking on the headers to expand or collapse the corresponding sections.

How to use it:

1. Build the accordion’s structure with HTML

  • Use the <details> to encapsulate each accordion item.
  • The <summary> element within each <details> acts as the clickable header.
<div id="menu" class="accordion">
  <details class="accordion__item" open>
    <summary class="accordion__item-btn">CSSScript.Com</summary>
    <div class="accordion__item-desc">
      A website features latest Vanilla JavaScript & Pure CSS projects.
    </div>
  </details>
  <details class="accordion__item">
    <summary class="accordion__item-btn">jQueryScript.Net</summary>
    <div class="accordion__item-desc">
      10,000+ latest free jQuery plugins for developers.
    </div>
  </details>
  ... more items here ...
</div>

2. Add the necessary CSS/CSS3 styles to the accordion to create a modern design.

/* Override default CSS variables here */
:root {
  --hue: 223;
  --bg: hsl(var(--hue),90%,90%);
  --fg: hsl(var(--hue),90%,10%);
  --shade1: hsl(var(--hue),10%,90%);
  --shade2: hsl(var(--hue),90%,80%);
  --shade3: hsl(var(--hue),90%,10%);
  --shade4: hsl(283,90%,80%);
  --desc-text1: hsl(var(--hue),10%,30%);
  --desc-text2: hsl(var(--hue),10%,70%);
  --trans-dur: 0.3s;
  --ease-in-out: cubic-bezier(0.65,0,0.35,1);
  --ease-out: cubic-bezier(0.33,1,0.68,1);
  font-size: calc(28px + (60 - 28) * (100vw - 280px) / (3840 - 280));
}
/* Accordion styles */
.accordion {
  --anim-dur: 0.5s;
  margin: auto;
  width: 100%;
  max-width: 18em;
}
.accordion__item {
  background-color: var(--shade1);
  transition: background-color var(--trans-dur);
}
.accordion__item-btn {
  background-color: transparent;
  box-shadow: 0 0 0 0.125em hsla(var(--hue), 90%, 50%, 0) inset;
  cursor: pointer;
  list-style: none;
  outline: transparent;
  padding: 0.25em 0.5em;
  position: relative;
  text-align: left;
  width: 100%;
  transition: box-shadow calc(var(--trans-dur) / 2) var(--ease-in-out);
  -webkit-tap-highlight-color: transparent;
}
.accordion__item-btn:focus-visible {
  box-shadow: 0 0 0 0.125em hsla(var(--hue), 90%, 50%, 1) inset;
}
.accordion__item-btn:before, .accordion__item-btn:after {
  background-color: currentColor;
  content: "";
  display: block;
  position: absolute;
  top: 50%;
  right: 0.5em;
  width: 0.75em;
  height: 1px;
  transition: transform var(--trans-dur) var(--ease-in-out);
}
.accordion__item-btn:after {
  transform: rotate(-90deg);
}
.accordion__item-btn::marker, .accordion__item-btn::-webkit-details-marker {
  display: none;
}
.accordion__item-desc {
  color: var(--desc-text1);
  min-height: 5.5em;
  padding: 0 0.5em 1em 0.5em;
  transition: color var(--trans-dur);
}
.accordion__item:nth-child(2) {
  background-color: var(--shade2);
}
.accordion__item:nth-child(3) {
  background-color: var(--shade3);
}
.accordion__item:nth-child(3) .accordion__item-btn {
  color: var(--bg);
  transition: color var(--trans-dur);
}
.accordion__item:nth-child(3) .accordion__item-desc {
  color: var(--desc-text2);
}
.accordion__item:nth-child(4) {
  background-color: var(--shade4);
}
.accordion__item.collapsing, .accordion__item.expanding {
  overflow: hidden;
}
.accordion__item[open] .accordion__item-btn:after {
  transform: rotate(0);
}
.accordion__item.expanding .accordion__item-btn:after {
  animation: accordion-minus var(--anim-dur) var(--ease-out) forwards;
  transform: rotate(0);
}
.accordion__item.expanding .accordion__item-desc {
  animation: accordion-fade-in var(--anim-dur) var(--ease-out) forwards;
}
.accordion__item.collapsing .accordion__item-btn:after {
  animation: accordion-plus var(--anim-dur) var(--ease-out) forwards;
  transform: rotate(-90deg);
}
.accordion__item.collapsing .accordion__item-desc {
  animation: accordion-fade-slide-out var(--anim-dur) var(--ease-out) forwards;
}
/* Dark theme */
@media (prefers-color-scheme: dark) {
  :root {
    --bg: hsl(var(--hue),90%,10%);
    --fg: hsl(var(--hue),90%,90%);
    --shade1: hsl(var(--hue),10%,20%);
    --shade2: hsl(var(--hue),90%,30%);
    --shade3: hsl(var(--hue),90%,90%);
    --shade4: hsl(283,90%,30%);
    --desc-text1: hsl(var(--hue),10%,80%);
    --desc-text2: hsl(var(--hue),10%,30%);
  }
}
/* Animations */
@keyframes accordion-minus {
  from {
    transform: rotate(-90deg);
  }
  to {
    transform: rotate(0);
  }
}
@keyframes accordion-plus {
  from {
    transform: rotate(0);
  }
  to {
    transform: rotate(-90deg);
  }
}
@keyframes accordion-fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}
@keyframes accordion-fade-slide-out {
  from {
    opacity: 1;
    transform: translateY(0);
  }
  to {
    opacity: 0;
    transform: translateY(-0.75em);
  }
}

3. Handle the interactive aspects of the accordion using the JavaScript as displayed below. It listens for click events on the <summary> elements and toggles the open state of the corresponding <details> element. Additionally, it calculates the height of the content and applies animations for a smooth expansion and collapse effect.

window.addEventListener("DOMContentLoaded",() => {
  const menu = new Accordion("#menu");
});
class Accordion {
  /** Element used for this accordion */
  el: HTMLElement | null;
  /** Accordion item array */
  items: AccordionItem[] = [];
  /**
   * @param el CSS selector of the accordion
   */
  constructor(el: string) {
    this.el = document.querySelector(el);
    const itemEls = Array.from(this.el?.querySelectorAll("details") || []);
    itemEls.forEach((itemEl,i) => {
      const id = `${i}`;
      itemEl.setAttribute("data-item",id)
      this.items.push(new AccordionItem(id,this));
    });
  }
}
class AccordionItem {
  /** Accordion object to which the item belongs */
  parent: Accordion;
  /** `<details>` used for the accordion item */
  el: HTMLDetailsElement | null | undefined;
  /** `<summary>` of the `<details>` */
  summary: HTMLElement | null | undefined;
  /** Content element succeeding the `<summary>` */
  content: HTMLElement | null | undefined;
  /** Element is collapsing */
  isCollapsing = false;
  /** Element is expanding */
  isExpanding = false;
  /** Animation object */
  animation?: Animation | null;
  /** Animation duration and easing */
  animParams: AnimParams = {
    duration: 500,
    easing: "cubic-bezier(0.33,1,0.68,1)"
  };
  /** Actions to run after expanding the item. */
  animActionsExpand: AnimActions = {
    onfinish: this.onAnimationFinish.bind(this,true),
    oncancel: () => { this.isExpanding = false; }
  };
  /** Actions to run after collapsing the item. */
  animActionsCollapse: AnimActions = {
    onfinish: this.onAnimationFinish.bind(this,false),
    oncancel: () => { this.isCollapsing = false; }
  };
  /** Close any open items. */
  closePrevious(): void {
    const openItems = this.parent.items.filter(item => item.isExpanding || item.el?.open);
    openItems?.forEach(item => item.collapse());
  }
  /**
   * @param id ID of the accordion item
   * @param parent Accordion object to which the item belongs
   */
  constructor(id: string, parent: Accordion) {
    this.parent = parent;
    this.el = this.parent.el?.querySelector(`[data-item="${id}"]`);
    this.summary = this.el?.querySelector("summary");
    this.summary?.addEventListener("click", this.toggle.bind(this));
    this.content = this.summary?.nextElementSibling as HTMLElement;
  }
  /**
   * Open or close the accordion.
   * @param e Click event whose default behavior should be prevented
   */
  toggle(e?: Event) {
    e?.preventDefault();
    this.el?.classList.remove("collapsing","expanding");
    const detailsClicked = (e?.target as HTMLElement).parentElement;
    const dataItemClicked = detailsClicked?.getAttribute("data-item");
    const detailsOpen = this.el?.parentElement?.querySelector("[open]");
    const dataItemOpen = detailsOpen?.getAttribute("data-item");
    if (dataItemClicked !== dataItemOpen) {
      // run the pre-toggle action only if a different item is clicked
      this.closePrevious();
    }
    if (this.isCollapsing || !this.el?.open) {
      this.open();
    } else if (this.isExpanding || this.el?.open) {
      this.collapse();
    }
  }
  /** Open the item and run the animation for expanding. */
  open(): void {
    if (this.el) {
      this.el.style.height = `${this.el.offsetHeight}px`;
      this.el.open = true;
      this.expand();
    }
  }
  /** Expansion animation */
  expand(): void {
    this.el?.classList.add("expanding");
    this.isExpanding = true;
    const startHeight = this.el?.offsetHeight || 0;
    const summaryHeight = this.summary?.offsetHeight || 0;
    const contentHeight = this.content?.offsetHeight || 0;
    const endHeight = summaryHeight + contentHeight;
    this.animation?.cancel();
    this.animation = this.el?.animate(
      { height: [`${startHeight}px`, `${endHeight}px`] },
      this.animParams
    );
    if (this.animation) {
      this.animation.onfinish = this.animActionsExpand.onfinish;
      this.animation.oncancel = this.animActionsExpand.oncancel;
    }
  }
  /** Close the item and run the animation for collapsing. */
  collapse(): void {
    this.el?.classList.add("collapsing");
    this.isCollapsing = true;
    const startHeight = this.el?.offsetHeight || 0;
    const endHeight = this.summary?.offsetHeight || 0;
    this.animation?.cancel();
    this.animation = this.el?.animate(
      { height: [`${startHeight}px`, `${endHeight}px`] },
      this.animParams
    );
    if (this.animation) {
      this.animation.onfinish = this.animActionsCollapse.onfinish;
      this.animation.oncancel = this.animActionsCollapse.oncancel;
    }
  }
  /** Actions to run when the animation is finished */
  onAnimationFinish(open: boolean): void {
    if (this.el) {
      this.el.open = open;
      if (this.animation) {
        this.animation = null;
      }
      this.isCollapsing = false;
      this.isExpanding = false;
      this.el.style.height = "";
      this.el.classList.remove("collapsing","expanding");
    }
  }
}
type AnimActions = {
  onfinish: () => void,
  oncancel: () => void
}
type AnimParams = {
  duration: number,
  easing: string
}

You Might Be Interested In:


Leave a Reply