Accordion

Collapsible content sections for organized information.

Optional icon: place an element with data-accordion-icon inside the trigger – it will rotate 180° when the panel opens.

Usage

Include the JavaScript code in your project:

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

window.init_accordions = init_accordions;

Then initialise all accordions on the page:

init_accordions();

You can pass global options directly to the function – see the Params section.

Auto‑initialisation

Add this at the end of your accordion.js file:

document.addEventListener("DOMContentLoaded", () => init_accordions());

Then link the script in your HTML:

<script src="accordion.js"></script>

Params

The init_accordions() function accepts an optional configuration object:

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

Data attributes

You can also configure individual accordions via data attributes on the container:

  • data-accordion="open" – allows multiple panels to be open at once (same as alwaysOpen: true).
  • data-active-classes – space‑separated CSS classes applied to an open trigger (default: "active").
  • data-inactive-classes – classes for a closed trigger (default: "inactive").

These attributes override the global options passed to init_accordions() for that specific accordion.