Accordion

Collapsible content sections for organized information.

To use this component copy our js code:

class Accordion {
  constructor(container, items, options) {
    // Save container element and accordion items
    this.container = container;
    this.items = items;

    // Merge default options with user-defined options
    this.options = {
      always_open: false, // Allow multiple panels open at the same time
      active_classes: "active", // CSS classes for active (open) state
      inactive_classes: "inactive", // CSS classes for inactive (closed) state
      on_open: () => {}, // Callback when a panel is opened
      on_close: () => {}, // Callback when a panel is closed
      on_toggle: () => {}, // Callback when a panel is toggled
      ...options,
    };

    // Initialize accordion
    this.init();
  }

  init() {
    if (this.items?.length > 0) {
      // Initialize each accordion item
      this.items.forEach((item) => {
        // Open item if it is marked as active
        if (item.active) this.open(item.id);

        // Add click event to toggle item on trigger click
        item.trigger.addEventListener("click", () => this.toggle(item.id));
      });
    }
  }

  // Find item by its id
  get_item(id) {
    return this.items.find((i) => i.id === id);
  }

  // Open a specific item
  open(id) {
    const item = this.get_item(id);

    // If multiple panels are not allowed, close all others
    if (!this.options.always_open) {
      this.items.forEach((i) => {
        if (i !== item) this.close(i.id);
      });
    }

    // Add active classes and remove inactive ones
    item.trigger.classList.add(...this.options.active_classes.split(" "));
    item.trigger.classList.remove(...this.options.inactive_classes.split(" "));

    // Update accessibility attribute
    item.trigger.setAttribute("aria-expanded", "true");

    // Show the panel
    item.panel.classList.remove("hidden");
    item.active = true;

    // Rotate back icon if exists
    if (item.icon) item.icon.classList.remove("rotate-180");

    // Run user-defined on_open callback
    this.options.on_open(this, item);
  }

  // Close a specific item
  close(id) {
    const item = this.get_item(id);

    // Remove active classes and add inactive ones
    item.trigger.classList.remove(...this.options.active_classes.split(" "));
    item.trigger.classList.add(...this.options.inactive_classes.split(" "));

    // Hide the panel
    item.panel.classList.add("hidden");
    item.trigger.setAttribute("aria-expanded", "false");
    item.active = false;

    // Rotate icon if exists
    if (item.icon) item.icon.classList.add("rotate-180");

    // Run user-defined on_close callback
    this.options.on_close(this, item);
  }

  // Toggle a specific item (open if closed, close if open)
  toggle(id) {
    const item = this.get_item(id);

    item.active ? this.close(id) : this.open(id);

    // Run user-defined on_toggle callback
    this.options.on_toggle(this, item);
  }
}

// Initialize all accordions on the page
export const init_accordions = (options = {}) => {
  document.querySelectorAll("[data-accordion]").forEach((container) => {
    // Read options from container attributes
    const always_open = container.getAttribute("data-accordion") === "open";
    const active_classes =
      container.getAttribute("data-active-classes") || "active";
    const inactive_classes =
      container.getAttribute("data-inactive-classes") || "inactive";

    // Collect all accordion items inside this container
    const items = [];
    container
      .querySelectorAll("[data-accordion-trigger]")
      .forEach((trigger) => {
        // Ensure the trigger belongs to the current accordion (not nested one)
        if (trigger.closest("[data-accordion]") === container) {
          const id = trigger.getAttribute("data-accordion-trigger");
          items.push({
            id, // Target panel ID
            trigger, // Button element
            panel: document.querySelector(id), // Target panel element
            icon: trigger.querySelector("[data-accordion-icon]"), // Optional icon
            active: trigger.getAttribute("aria-expanded") === "true", // Initial state
          });
        }
      });

    // Create a new accordion instance
    new Accordion(container, items, {
      always_open,
      active_classes,
      inactive_classes,
      ...options,
    });
  });
};

Then init accordions:

init_accordions();

Params

Available parameters for use in init_accordions:

{
  alwaysOpen: false, // Allow multiple panels open at the same time
  activeClasses: "active", // CSS classes for active (open) state
  inactiveClasses: "inactive", // CSS classes for inactive (closed) state
  onOpen: () => {}, // Callback when a panel is opened
  onClose: () => {}, // Callback when a panel is closed
  onToggle: () => {}, // Callback when a panel is toggled
};

Usage

Text