accordion.js

I’ll build this as a reusable, advanced accordion.js for your RX Theme: accessible ARIA behavior, multiple accordion groups, keyboard support, auto-close options, URL hash opening, animations, nested support, and safe initialization.

My dear friend, create this file:

rx-theme/inc/js/accordion.js

Then paste the full code below.

/*!
 * RX Theme Advanced Accordion
 * File: inc/js/accordion.js
 * Theme: RX Theme
 *
 * Features:
 * - Accessible accordion with ARIA attributes
 * - Multiple accordion groups on one page
 * - Single-open or multi-open mode
 * - Smooth open/close animation
 * - Keyboard navigation
 * - URL hash open support
 * - Nested accordion support
 * - Auto-close outside click option
 * - Remember opened accordion with localStorage
 * - Auto-init with data attributes
 * - Custom events for developers
 */

(function () {
  'use strict';

  /**
   * RX Accordion Default Settings
   */
  var RX_ACCORDION_DEFAULTS = {
    rootSelector: '[data-rx-accordion]',
    itemSelector: '[data-rx-accordion-item]',
    triggerSelector: '[data-rx-accordion-trigger]',
    panelSelector: '[data-rx-accordion-panel]',

    activeClass: 'is-active',
    openingClass: 'is-opening',
    closingClass: 'is-closing',
    disabledClass: 'is-disabled',

    singleOpen: false,
    closeAllOnInit: false,
    openFirst: false,
    allowAllClosed: true,

    animation: true,
    animationDuration: 300,
    easing: 'ease',

    keyboard: true,
    hashOpen: true,
    scrollToOpened: false,
    scrollOffset: 90,

    closeOnOutsideClick: false,
    closeOnEscape: true,

    rememberState: false,
    storagePrefix: 'rx_accordion_',

    nested: true,
    autoId: true,

    debug: false
  };

  /**
   * Small utility object
   */
  var RXAccordionUtils = {
    extend: function () {
      var output = {};

      for (var i = 0; i < arguments.length; i++) {
        var obj = arguments[i];

        if (!obj) {
          continue;
        }

        for (var key in obj) {
          if (Object.prototype.hasOwnProperty.call(obj, key)) {
            output[key] = obj[key];
          }
        }
      }

      return output;
    },

    toBoolean: function (value, fallback) {
      if (value === undefined || value === null || value === '') {
        return fallback;
      }

      if (typeof value === 'boolean') {
        return value;
      }

      if (typeof value === 'string') {
        value = value.toLowerCase().trim();

        if (value === 'true' || value === '1' || value === 'yes' || value === 'on') {
          return true;
        }

        if (value === 'false' || value === '0' || value === 'no' || value === 'off') {
          return false;
        }
      }

      return fallback;
    },

    toNumber: function (value, fallback) {
      var number = parseInt(value, 10);
      return isNaN(number) ? fallback : number;
    },

    isElement: function (element) {
      return element instanceof Element || element instanceof HTMLDocument;
    },

    closest: function (element, selector) {
      if (!element || !selector) {
        return null;
      }

      if (element.closest) {
        return element.closest(selector);
      }

      while (element) {
        if (element.matches && element.matches(selector)) {
          return element;
        }

        element = element.parentElement;
      }

      return null;
    },

    matches: function (element, selector) {
      if (!element || !selector) {
        return false;
      }

      var proto = Element.prototype;
      var fn =
        proto.matches ||
        proto.matchesSelector ||
        proto.msMatchesSelector ||
        proto.webkitMatchesSelector;

      return fn.call(element, selector);
    },

    getFocusableElements: function (container) {
      if (!container) {
        return [];
      }

      var selectors = [
        'a[href]',
        'area[href]',
        'button:not([disabled])',
        'input:not([disabled])',
        'select:not([disabled])',
        'textarea:not([disabled])',
        'iframe',
        'object',
        'embed',
        '[contenteditable]',
        '[tabindex]:not([tabindex="-1"])'
      ];

      return Array.prototype.slice.call(container.querySelectorAll(selectors.join(',')))
        .filter(function (element) {
          return !!(
            element.offsetWidth ||
            element.offsetHeight ||
            element.getClientRects().length
          );
        });
    },

    uniqueId: function (prefix) {
      prefix = prefix || 'rx-accordion';
      return prefix + '-' + Math.random().toString(36).substr(2, 9);
    },

    dispatch: function (element, eventName, detail) {
      if (!element) {
        return;
      }

      var event;

      if (typeof window.CustomEvent === 'function') {
        event = new CustomEvent(eventName, {
          bubbles: true,
          cancelable: true,
          detail: detail || {}
        });
      } else {
        event = document.createEvent('CustomEvent');
        event.initCustomEvent(eventName, true, true, detail || {});
      }

      element.dispatchEvent(event);
    },

    log: function (enabled) {
      if (!enabled || !window.console) {
        return;
      }

      var args = Array.prototype.slice.call(arguments, 1);
      console.log.apply(console, args);
    },

    escapeSelector: function (value) {
      if (!value) {
        return '';
      }

      if (window.CSS && CSS.escape) {
        return CSS.escape(value);
      }

      return value.replace(/([ #;?%&,.+*~\':"!^$[\]()=>|\/@])/g, '\\$1');
    }
  };

  /**
   * RXAccordion Class
   */
  function RXAccordion(root, options) {
    if (!RXAccordionUtils.isElement(root)) {
      return;
    }

    this.root = root;
    this.settings = RXAccordionUtils.extend(
      {},
      RX_ACCORDION_DEFAULTS,
      this.getDataOptions(),
      options || {}
    );

    this.items = [];
    this.triggers = [];
    this.panels = [];
    this.id = this.root.getAttribute('id') || RXAccordionUtils.uniqueId('rx-accordion');
    this.initialized = false;
    this.destroyed = false;

    this.onClickBound = this.onClick.bind(this);
    this.onKeydownBound = this.onKeydown.bind(this);
    this.onDocumentClickBound = this.onDocumentClick.bind(this);
    this.onHashChangeBound = this.onHashChange.bind(this);

    this.init();
  }

  /**
   * Get options from HTML data attributes
   */
  RXAccordion.prototype.getDataOptions = function () {
    var dataset = this.root.dataset || {};

    return {
      singleOpen: RXAccordionUtils.toBoolean(dataset.rxAccordionSingle, RX_ACCORDION_DEFAULTS.singleOpen),
      closeAllOnInit: RXAccordionUtils.toBoolean(dataset.rxAccordionCloseAll, RX_ACCORDION_DEFAULTS.closeAllOnInit),
      openFirst: RXAccordionUtils.toBoolean(dataset.rxAccordionOpenFirst, RX_ACCORDION_DEFAULTS.openFirst),
      allowAllClosed: RXAccordionUtils.toBoolean(dataset.rxAccordionAllowAllClosed, RX_ACCORDION_DEFAULTS.allowAllClosed),
      animation: RXAccordionUtils.toBoolean(dataset.rxAccordionAnimation, RX_ACCORDION_DEFAULTS.animation),
      animationDuration: RXAccordionUtils.toNumber(dataset.rxAccordionDuration, RX_ACCORDION_DEFAULTS.animationDuration),
      keyboard: RXAccordionUtils.toBoolean(dataset.rxAccordionKeyboard, RX_ACCORDION_DEFAULTS.keyboard),
      hashOpen: RXAccordionUtils.toBoolean(dataset.rxAccordionHash, RX_ACCORDION_DEFAULTS.hashOpen),
      scrollToOpened: RXAccordionUtils.toBoolean(dataset.rxAccordionScroll, RX_ACCORDION_DEFAULTS.scrollToOpened),
      closeOnOutsideClick: RXAccordionUtils.toBoolean(dataset.rxAccordionOutside, RX_ACCORDION_DEFAULTS.closeOnOutsideClick),
      closeOnEscape: RXAccordionUtils.toBoolean(dataset.rxAccordionEscape, RX_ACCORDION_DEFAULTS.closeOnEscape),
      rememberState: RXAccordionUtils.toBoolean(dataset.rxAccordionRemember, RX_ACCORDION_DEFAULTS.rememberState),
      debug: RXAccordionUtils.toBoolean(dataset.rxAccordionDebug, RX_ACCORDION_DEFAULTS.debug)
    };
  };

  /**
   * Initialize accordion
   */
  RXAccordion.prototype.init = function () {
    if (this.initialized || this.destroyed) {
      return;
    }

    this.root.setAttribute('data-rx-accordion-initialized', 'true');

    if (!this.root.getAttribute('id')) {
      this.root.setAttribute('id', this.id);
    }

    this.collectItems();

    if (!this.items.length) {
      RXAccordionUtils.log(this.settings.debug, 'RX Accordion: No items found.', this.root);
      return;
    }

    this.setupAccessibility();
    this.bindEvents();
    this.applyInitialState();

    this.initialized = true;

    RXAccordionUtils.dispatch(this.root, 'rxAccordion:init', {
      accordion: this
    });

    RXAccordionUtils.log(this.settings.debug, 'RX Accordion initialized:', this.root);
  };

  /**
   * Collect direct accordion items
   */
  RXAccordion.prototype.collectItems = function () {
    var allItems = Array.prototype.slice.call(
      this.root.querySelectorAll(this.settings.itemSelector)
    );

    var self = this;

    this.items = allItems.filter(function (item) {
      if (self.settings.nested) {
        var parentAccordion = RXAccordionUtils.closest(
          item.parentElement,
          self.settings.rootSelector
        );

        return parentAccordion === self.root;
      }

      return true;
    });

    this.triggers = [];
    this.panels = [];

    this.items.forEach(function (item) {
      var trigger = self.getItemTrigger(item);
      var panel = self.getItemPanel(item);

      if (trigger && panel) {
        self.triggers.push(trigger);
        self.panels.push(panel);
      }
    });
  };

  /**
   * Get trigger inside item
   */
  RXAccordion.prototype.getItemTrigger = function (item) {
    if (!item) {
      return null;
    }

    var triggers = Array.prototype.slice.call(
      item.querySelectorAll(this.settings.triggerSelector)
    );

    var self = this;

    return triggers.filter(function (trigger) {
      if (self.settings.nested) {
        return RXAccordionUtils.closest(trigger, self.settings.itemSelector) === item;
      }

      return true;
    })[0] || null;
  };

  /**
   * Get panel inside item
   */
  RXAccordion.prototype.getItemPanel = function (item) {
    if (!item) {
      return null;
    }

    var panels = Array.prototype.slice.call(
      item.querySelectorAll(this.settings.panelSelector)
    );

    var self = this;

    return panels.filter(function (panel) {
      if (self.settings.nested) {
        return RXAccordionUtils.closest(panel, self.settings.itemSelector) === item;
      }

      return true;
    })[0] || null;
  };

  /**
   * Setup ARIA attributes
   */
  RXAccordion.prototype.setupAccessibility = function () {
    var self = this;

    this.items.forEach(function (item, index) {
      var trigger = self.getItemTrigger(item);
      var panel = self.getItemPanel(item);

      if (!trigger || !panel) {
        return;
      }

      var triggerId = trigger.getAttribute('id');
      var panelId = panel.getAttribute('id');

      if (self.settings.autoId) {
        if (!triggerId) {
          triggerId = self.id + '-trigger-' + index;
          trigger.setAttribute('id', triggerId);
        }

        if (!panelId) {
          panelId = self.id + '-panel-' + index;
          panel.setAttribute('id', panelId);
        }
      }

      trigger.setAttribute('type', trigger.tagName.toLowerCase() === 'button' ? 'button' : trigger.getAttribute('type') || 'button');
      trigger.setAttribute('aria-controls', panelId);
      trigger.setAttribute('aria-expanded', 'false');

      panel.setAttribute('role', 'region');
      panel.setAttribute('aria-labelledby', triggerId);
      panel.setAttribute('hidden', '');

      item.setAttribute('data-rx-accordion-index', index);
    });
  };

  /**
   * Bind events
   */
  RXAccordion.prototype.bindEvents = function () {
    this.root.addEventListener('click', this.onClickBound);

    if (this.settings.keyboard) {
      this.root.addEventListener('keydown', this.onKeydownBound);
    }

    if (this.settings.closeOnOutsideClick) {
      document.addEventListener('click', this.onDocumentClickBound);
    }

    if (this.settings.hashOpen) {
      window.addEventListener('hashchange', this.onHashChangeBound);
    }
  };

  /**
   * Remove events
   */
  RXAccordion.prototype.unbindEvents = function () {
    this.root.removeEventListener('click', this.onClickBound);
    this.root.removeEventListener('keydown', this.onKeydownBound);
    document.removeEventListener('click', this.onDocumentClickBound);
    window.removeEventListener('hashchange', this.onHashChangeBound);
  };

  /**
   * Apply initial state
   */
  RXAccordion.prototype.applyInitialState = function () {
    var openedFromStorage = false;
    var openedFromHash = false;
    var self = this;

    if (this.settings.closeAllOnInit) {
      this.closeAll(false);
    }

    this.items.forEach(function (item) {
      var shouldOpen = RXAccordionUtils.toBoolean(
        item.getAttribute('data-rx-accordion-open'),
        false
      );

      if (item.classList.contains(self.settings.activeClass)) {
        shouldOpen = true;
      }

      if (shouldOpen) {
        self.openItem(item, false);
      } else {
        self.closeItem(item, false);
      }
    });

    if (this.settings.rememberState) {
      openedFromStorage = this.restoreState();
    }

    if (this.settings.hashOpen && window.location.hash) {
      openedFromHash = this.openFromHash(false);
    }

    if (!openedFromStorage && !openedFromHash && this.settings.openFirst) {
      this.openItem(this.items[0], false);
    }

    if (this.settings.singleOpen) {
      var openItems = this.getOpenItems();

      if (openItems.length > 1) {
        openItems.slice(1).forEach(function (item) {
          self.closeItem(item, false);
        });
      }
    }
  };

  /**
   * Click handler
   */
  RXAccordion.prototype.onClick = function (event) {
    var trigger = RXAccordionUtils.closest(event.target, this.settings.triggerSelector);

    if (!trigger || !this.root.contains(trigger)) {
      return;
    }

    var item = RXAccordionUtils.closest(trigger, this.settings.itemSelector);

    if (!item || this.items.indexOf(item) === -1) {
      return;
    }

    if (this.isDisabled(item, trigger)) {
      event.preventDefault();
      return;
    }

    event.preventDefault();
    this.toggleItem(item, true);
  };

  /**
   * Keyboard handler
   */
  RXAccordion.prototype.onKeydown = function (event) {
    var trigger = RXAccordionUtils.closest(event.target, this.settings.triggerSelector);

    if (!trigger || this.triggers.indexOf(trigger) === -1) {
      return;
    }

    var key = event.key || event.keyCode;
    var currentIndex = this.triggers.indexOf(trigger);
    var nextIndex = null;

    switch (key) {
      case 'ArrowDown':
      case 40:
        nextIndex = currentIndex + 1;
        if (nextIndex >= this.triggers.length) {
          nextIndex = 0;
        }
        event.preventDefault();
        this.focusTrigger(nextIndex);
        break;

      case 'ArrowUp':
      case 38:
        nextIndex = currentIndex - 1;
        if (nextIndex < 0) {
          nextIndex = this.triggers.length - 1;
        }
        event.preventDefault();
        this.focusTrigger(nextIndex);
        break;

      case 'Home':
      case 36:
        event.preventDefault();
        this.focusTrigger(0);
        break;

      case 'End':
      case 35:
        event.preventDefault();
        this.focusTrigger(this.triggers.length - 1);
        break;

      case 'Enter':
      case 13:
      case ' ':
      case 32:
        event.preventDefault();
        this.toggleItemByTrigger(trigger, true);
        break;

      case 'Escape':
      case 27:
        if (this.settings.closeOnEscape) {
          event.preventDefault();
          this.closeItemByTrigger(trigger, true);
          trigger.focus();
        }
        break;

      default:
        break;
    }
  };

  /**
   * Document click handler
   */
  RXAccordion.prototype.onDocumentClick = function (event) {
    if (!this.root.contains(event.target)) {
      this.closeAll(true);
    }
  };

  /**
   * Hash change handler
   */
  RXAccordion.prototype.onHashChange = function () {
    this.openFromHash(true);
  };

  /**
   * Focus trigger by index
   */
  RXAccordion.prototype.focusTrigger = function (index) {
    var trigger = this.triggers[index];

    if (trigger && !trigger.disabled) {
      trigger.focus();
    }
  };

  /**
   * Toggle by trigger
   */
  RXAccordion.prototype.toggleItemByTrigger = function (trigger, animate) {
    var item = RXAccordionUtils.closest(trigger, this.settings.itemSelector);

    if (item) {
      this.toggleItem(item, animate);
    }
  };

  /**
   * Close by trigger
   */
  RXAccordion.prototype.closeItemByTrigger = function (trigger, animate) {
    var item = RXAccordionUtils.closest(trigger, this.settings.itemSelector);

    if (item) {
      this.closeItem(item, animate);
    }
  };

  /**
   * Toggle item
   */
  RXAccordion.prototype.toggleItem = function (item, animate) {
    if (this.isOpen(item)) {
      if (!this.settings.allowAllClosed && this.getOpenItems().length <= 1) {
        return;
      }

      this.closeItem(item, animate);
    } else {
      this.openItem(item, animate);
    }
  };

  /**
   * Open item
   */
  RXAccordion.prototype.openItem = function (item, animate) {
    if (!item || this.items.indexOf(item) === -1) {
      return;
    }

    var trigger = this.getItemTrigger(item);
    var panel = this.getItemPanel(item);

    if (!trigger || !panel || this.isDisabled(item, trigger)) {
      return;
    }

    if (this.isOpen(item)) {
      return;
    }

    var self = this;

    if (this.settings.singleOpen) {
      this.items.forEach(function (otherItem) {
        if (otherItem !== item) {
          self.closeItem(otherItem, animate);
        }
      });
    }

    RXAccordionUtils.dispatch(this.root, 'rxAccordion:beforeOpen', {
      accordion: this,
      item: item,
      trigger: trigger,
      panel: panel
    });

    item.classList.add(this.settings.activeClass);
    item.classList.add(this.settings.openingClass);
    item.classList.remove(this.settings.closingClass);

    trigger.setAttribute('aria-expanded', 'true');
    panel.removeAttribute('hidden');

    if (this.settings.animation && animate !== false) {
      this.animateOpen(panel, function () {
        item.classList.remove(self.settings.openingClass);

        RXAccordionUtils.dispatch(self.root, 'rxAccordion:open', {
          accordion: self,
          item: item,
          trigger: trigger,
          panel: panel
        });

        self.saveState();

        if (self.settings.scrollToOpened) {
          self.scrollToItem(item);
        }
      });
    } else {
      panel.style.height = '';
      panel.style.overflow = '';
      item.classList.remove(this.settings.openingClass);

      RXAccordionUtils.dispatch(this.root, 'rxAccordion:open', {
        accordion: this,
        item: item,
        trigger: trigger,
        panel: panel
      });

      this.saveState();

      if (this.settings.scrollToOpened) {
        this.scrollToItem(item);
      }
    }
  };

  /**
   * Close item
   */
  RXAccordion.prototype.closeItem = function (item, animate) {
    if (!item || this.items.indexOf(item) === -1) {
      return;
    }

    var trigger = this.getItemTrigger(item);
    var panel = this.getItemPanel(item);

    if (!trigger || !panel) {
      return;
    }

    if (!this.isOpen(item)) {
      panel.setAttribute('hidden', '');
      trigger.setAttribute('aria-expanded', 'false');
      return;
    }

    RXAccordionUtils.dispatch(this.root, 'rxAccordion:beforeClose', {
      accordion: this,
      item: item,
      trigger: trigger,
      panel: panel
    });

    var self = this;

    item.classList.add(this.settings.closingClass);
    item.classList.remove(this.settings.openingClass);

    trigger.setAttribute('aria-expanded', 'false');

    if (this.settings.animation && animate !== false) {
      this.animateClose(panel, function () {
        item.classList.remove(self.settings.activeClass);
        item.classList.remove(self.settings.closingClass);
        panel.setAttribute('hidden', '');

        RXAccordionUtils.dispatch(self.root, 'rxAccordion:close', {
          accordion: self,
          item: item,
          trigger: trigger,
          panel: panel
        });

        self.saveState();
      });
    } else {
      item.classList.remove(this.settings.activeClass);
      item.classList.remove(this.settings.closingClass);
      panel.style.height = '';
      panel.style.overflow = '';
      panel.setAttribute('hidden', '');

      RXAccordionUtils.dispatch(this.root, 'rxAccordion:close', {
        accordion: this,
        item: item,
        trigger: trigger,
        panel: panel
      });

      this.saveState();
    }
  };

  /**
   * Open all items
   */
  RXAccordion.prototype.openAll = function (animate) {
    if (this.settings.singleOpen) {
      this.openItem(this.items[0], animate);
      return;
    }

    var self = this;

    this.items.forEach(function (item) {
      self.openItem(item, animate);
    });
  };

  /**
   * Close all items
   */
  RXAccordion.prototype.closeAll = function (animate) {
    if (!this.settings.allowAllClosed && this.getOpenItems().length <= 1) {
      return;
    }

    var self = this;

    this.items.forEach(function (item) {
      self.closeItem(item, animate);
    });
  };

  /**
   * Animate open
   */
  RXAccordion.prototype.animateOpen = function (panel, callback) {
    panel.style.removeProperty('display');
    panel.style.overflow = 'hidden';
    panel.style.height = '0px';
    panel.style.transition = 'height ' + this.settings.animationDuration + 'ms ' + this.settings.easing;

    panel.offsetHeight;

    var targetHeight = panel.scrollHeight;

    panel.style.height = targetHeight + 'px';

    var done = function () {
      panel.style.height = '';
      panel.style.overflow = '';
      panel.style.transition = '';
      panel.removeEventListener('transitionend', onEnd);

      if (typeof callback === 'function') {
        callback();
      }
    };

    var onEnd = function (event) {
      if (event.target === panel && event.propertyName === 'height') {
        done();
      }
    };

    panel.addEventListener('transitionend', onEnd);

    window.setTimeout(done, this.settings.animationDuration + 80);
  };

  /**
   * Animate close
   */
  RXAccordion.prototype.animateClose = function (panel, callback) {
    panel.style.overflow = 'hidden';
    panel.style.height = panel.scrollHeight + 'px';
    panel.style.transition = 'height ' + this.settings.animationDuration + 'ms ' + this.settings.easing;

    panel.offsetHeight;

    panel.style.height = '0px';

    var doneCalled = false;

    var done = function () {
      if (doneCalled) {
        return;
      }

      doneCalled = true;

      panel.style.height = '';
      panel.style.overflow = '';
      panel.style.transition = '';
      panel.removeEventListener('transitionend', onEnd);

      if (typeof callback === 'function') {
        callback();
      }
    };

    var onEnd = function (event) {
      if (event.target === panel && event.propertyName === 'height') {
        done();
      }
    };

    panel.addEventListener('transitionend', onEnd);

    window.setTimeout(done, this.settings.animationDuration + 80);
  };

  /**
   * Check open state
   */
  RXAccordion.prototype.isOpen = function (item) {
    if (!item) {
      return false;
    }

    return item.classList.contains(this.settings.activeClass);
  };

  /**
   * Check disabled state
   */
  RXAccordion.prototype.isDisabled = function (item, trigger) {
    if (!item) {
      return true;
    }

    if (item.classList.contains(this.settings.disabledClass)) {
      return true;
    }

    if (item.hasAttribute('data-rx-accordion-disabled')) {
      return true;
    }

    if (trigger && (trigger.disabled || trigger.getAttribute('aria-disabled') === 'true')) {
      return true;
    }

    return false;
  };

  /**
   * Get open items
   */
  RXAccordion.prototype.getOpenItems = function () {
    var self = this;

    return this.items.filter(function (item) {
      return self.isOpen(item);
    });
  };

  /**
   * Scroll to item
   */
  RXAccordion.prototype.scrollToItem = function (item) {
    if (!item) {
      return;
    }

    var rect = item.getBoundingClientRect();
    var top = rect.top + window.pageYOffset - this.settings.scrollOffset;

    window.scrollTo({
      top: top,
      behavior: 'smooth'
    });
  };

  /**
   * Open accordion from URL hash
   *
   * Works with:
   * #panel-id
   * #trigger-id
   * #item-id
   */
  RXAccordion.prototype.openFromHash = function (animate) {
    var hash = window.location.hash;

    if (!hash || hash.length < 2) {
      return false;
    }

    var id = decodeURIComponent(hash.substring(1));
    var selector = '#' + RXAccordionUtils.escapeSelector(id);
    var target;

    try {
      target = this.root.querySelector(selector);
    } catch (error) {
      return false;
    }

    if (!target) {
      return false;
    }

    var item = null;

    if (RXAccordionUtils.matches(target, this.settings.itemSelector)) {
      item = target;
    } else {
      item = RXAccordionUtils.closest(target, this.settings.itemSelector);
    }

    if (!item || this.items.indexOf(item) === -1) {
      return false;
    }

    this.openItem(item, animate);

    if (this.settings.scrollToOpened) {
      this.scrollToItem(item);
    }

    return true;
  };

  /**
   * Save opened state
   */
  RXAccordion.prototype.saveState = function () {
    if (!this.settings.rememberState || !window.localStorage) {
      return;
    }

    var key = this.settings.storagePrefix + this.id;
    var openIndexes = [];

    this.items.forEach(function (item, index) {
      if (item.classList.contains('is-active')) {
        openIndexes.push(index);
      }
    });

    try {
      window.localStorage.setItem(key, JSON.stringify(openIndexes));
    } catch (error) {
      RXAccordionUtils.log(this.settings.debug, 'RX Accordion storage save failed:', error);
    }
  };

  /**
   * Restore opened state
   */
  RXAccordion.prototype.restoreState = function () {
    if (!this.settings.rememberState || !window.localStorage) {
      return false;
    }

    var key = this.settings.storagePrefix + this.id;
    var stored;

    try {
      stored = window.localStorage.getItem(key);
    } catch (error) {
      return false;
    }

    if (!stored) {
      return false;
    }

    var indexes;

    try {
      indexes = JSON.parse(stored);
    } catch (error) {
      return false;
    }

    if (!Array.isArray(indexes)) {
      return false;
    }

    var self = this;
    var opened = false;

    indexes.forEach(function (index) {
      if (self.items[index]) {
        self.openItem(self.items[index], false);
        opened = true;
      }
    });

    return opened;
  };

  /**
   * Refresh accordion
   */
  RXAccordion.prototype.refresh = function () {
    this.collectItems();
    this.setupAccessibility();

    RXAccordionUtils.dispatch(this.root, 'rxAccordion:refresh', {
      accordion: this
    });
  };

  /**
   * Destroy accordion
   */
  RXAccordion.prototype.destroy = function () {
    if (this.destroyed) {
      return;
    }

    this.unbindEvents();

    this.items.forEach(function (item) {
      var trigger = item.querySelector('[data-rx-accordion-trigger]');
      var panel = item.querySelector('[data-rx-accordion-panel]');

      item.classList.remove('is-active', 'is-opening', 'is-closing');

      if (trigger) {
        trigger.removeAttribute('aria-expanded');
        trigger.removeAttribute('aria-controls');
      }

      if (panel) {
        panel.removeAttribute('role');
        panel.removeAttribute('aria-labelledby');
        panel.removeAttribute('hidden');
        panel.style.height = '';
        panel.style.overflow = '';
        panel.style.transition = '';
      }
    });

    this.root.removeAttribute('data-rx-accordion-initialized');

    RXAccordionUtils.dispatch(this.root, 'rxAccordion:destroy', {
      accordion: this
    });

    this.destroyed = true;
    this.initialized = false;
  };

  /**
   * Public plugin object
   */
  window.RXAccordion = RXAccordion;

  /**
   * Auto initializer
   */
  window.RXAccordionInit = function (context) {
    context = context || document;

    var accordions = Array.prototype.slice.call(
      context.querySelectorAll(RX_ACCORDION_DEFAULTS.rootSelector)
    );

    accordions.forEach(function (accordionRoot) {
      if (accordionRoot.getAttribute('data-rx-accordion-initialized') === 'true') {
        return;
      }

      accordionRoot.rxAccordion = new RXAccordion(accordionRoot);
    });
  };

  /**
   * DOM ready helper
   */
  function onReady(callback) {
    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', callback);
    } else {
      callback();
    }
  }

  /**
   * Auto start
   */
  onReady(function () {
    window.RXAccordionInit(document);
  });

  /**
   * Optional AJAX / dynamic content support
   *
   * Trigger this after loading dynamic content:
   * document.dispatchEvent(new CustomEvent('rxTheme:contentLoaded', { detail: { container: element } }));
   */
  document.addEventListener('rxTheme:contentLoaded', function (event) {
    var container = event.detail && event.detail.container ? event.detail.container : document;
    window.RXAccordionInit(container);
  });

})();

Example HTML structure

Use this in your theme template, post content, or block output:

<div
  class="rx-accordion"
  data-rx-accordion
  data-rx-accordion-single="true"
  data-rx-accordion-open-first="false"
  data-rx-accordion-animation="true"
  data-rx-accordion-duration="300"
  data-rx-accordion-hash="true"
  data-rx-accordion-remember="true"
>
  <div class="rx-accordion__item" data-rx-accordion-item>
    <button class="rx-accordion__trigger" data-rx-accordion-trigger>
      What is RX Theme?
    </button>

    <div class="rx-accordion__panel" data-rx-accordion-panel>
      <p>RX Theme is your personal WordPress medical theme project.</p>
    </div>
  </div>

  <div class="rx-accordion__item" data-rx-accordion-item>
    <button class="rx-accordion__trigger" data-rx-accordion-trigger>
      Is this accordion SEO friendly?
    </button>

    <div class="rx-accordion__panel" data-rx-accordion-panel>
      <p>Yes. The content remains in HTML and is accessible for users and search engines.</p>
    </div>
  </div>
</div>

Add this CSS in your theme style file

.rx-accordion {
  width: 100%;
  margin: 0 0 24px;
}

.rx-accordion__item {
  border: 1px solid #e5e7eb;
  border-radius: 10px;
  margin-bottom: 12px;
  background: #ffffff;
  overflow: hidden;
}

.rx-accordion__trigger {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 16px 18px;
  border: 0;
  background: #f8fafc;
  color: #111827;
  font-size: 17px;
  font-weight: 700;
  line-height: 1.4;
  text-align: left;
  cursor: pointer;
}

.rx-accordion__trigger:hover,
.rx-accordion__trigger:focus {
  background: #eef2ff;
  outline: none;
}

.rx-accordion__trigger::after {
  content: "+";
  flex: 0 0 auto;
  font-size: 22px;
  font-weight: 700;
  line-height: 1;
}

.rx-accordion__item.is-active .rx-accordion__trigger::after {
  content: "−";
}

.rx-accordion__panel {
  padding: 18px;
  background: #ffffff;
  color: #374151;
  font-size: 16px;
  line-height: 1.7;
}

.rx-accordion__panel[hidden] {
  display: none;
}

.rx-accordion__item.is-disabled {
  opacity: 0.55;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .rx-accordion__panel {
    transition: none !important;
  }
}

Enqueue it in functions.php

function rx_theme_enqueue_accordion_script() {
    wp_enqueue_script(
        'rx-theme-accordion',
        get_template_directory_uri() . '/inc/js/accordion.js',
        array(),
        wp_get_theme()->get( 'Version' ),
        true
    );
}
add_action( 'wp_enqueue_scripts', 'rx_theme_enqueue_accordion_script' );

Best settings examples

For FAQ accordion where only one answer opens at a time:

<div data-rx-accordion data-rx-accordion-single="true">

For medical article sections where many answers can stay open:

<div data-rx-accordion data-rx-accordion-single="false">

For opening the first item automatically:

<div data-rx-accordion data-rx-accordion-open-first="true">

For remembering opened items after page reload:

<div data-rx-accordion data-rx-accordion-remember="true">

For opening by URL hash:

<div data-rx-accordion data-rx-accordion-hash="true">

Example:

<div id="diagnosis-panel" data-rx-accordion-panel>

Then open directly with:

your-page-url/#diagnosis-panel

Leave a Reply

Your email address will not be published. Required fields are marked *