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