<accordion-group>
Create progressively enhanced accordion groups.
Basic Usage
Wrap a collection of headings and content in an <accordion-group>
element.
The <accordion-group>
element should have a [headings]
attribute, with a CSS selector string for the headings that will toggle the accordion behavior. Content should appear directly after it’s controlling heading in the markup.
<accordion-group headings="h2">
<h2>Yo, ho ho!</h2>
<div>Yo, ho ho and a bottle of rum!</div>
<h2>Ahoy, there!</h2>
<div>Ahoy there, matey!</div>
</accordion-group>
When the Web Component loads, it will hide the content, add required controls and ARIA attributes, and add event listeners to selectively show or hide the content when a heading is clicked.
Exclusive Accordions
By default, all triggers and content in an <accordion-group>
behave independently of each other. If you want to only allow one content are to be open at a time, add the [exclusive]
attribute to the <accordion-group>
.
<accordion-group headings="h2" exclusive>
<h2>Yo, ho ho!</h2>
<div>Yo, ho ho and a bottle of rum!</div>
<h2>Ahoy, there!</h2>
<div>Ahoy there, matey!</div>
</accordion-group>
Styling
The Web Component loads a global <style>
element that includes the minimum required styles to maintain header styling for the accordion buttons and display expand/collapse icons for visual affordance.
You can modify the appearance of the displayed icons using the --accordion-icon-expanded
and --accordion-icon-collapsed
CSS variables.
accordion-group {
--accordion-icon-expanded: " đ";
--accordion-icon-collapsed: " đ";
}
The Web Component
customElements.define('accordion-group', class extends HTMLElement {
/**
* Instantiate the Web Component
*/
constructor () {
// Get parent class properties
super();
// Set properties
this.headings = this.getAttribute('headings');
this.exclusive = this.hasAttribute('exclusive');
// Setup UI
this.setupDOM();
this.loadCSS();
this.handler = this.createHandler();
}
/**
* Add buttons and hide content on page load
*/
setupDOM () {
// Get all accordion headings
let headings = this.querySelectorAll(this.headings);
// Update content
for (let heading of headings) {
// Get the matching content
let content = heading.nextElementSibling;
if (!content) continue;
// Create a button, and copy heading content into it
let btn = document.createElement('button');
btn.innerHTML = heading.innerHTML;
// Wipe the heading content, and replace it with the button
heading.innerHTML = '';
heading.append(btn);
heading.setAttribute('trigger', '');
// Hide the content
content.setAttribute('hidden', '');
// Add ARIA
btn.setAttribute('aria-expanded', false);
}
}
/**
* Load accordion styles
*/
loadCSS () {
if (document.querySelector('[data-accordion-group-css]')) return;
let css = document.createElement('style');
css.innerHTML =
`/**
* Style the accordion buttons to look like headers
*/
accordion-group [trigger] > button {
background: transparent;
border: none;
display: block;
font: inherit;
margin: 0;
padding: 0;
text-align: left;
width: 100%;
}
/**
* Show expand/collapse icons
*/
accordion-group [trigger] > button[aria-expanded="true"]::after {
content: var(--accordion-icon-expanded, " â");
}
accordion-group [trigger] > button[aria-expanded="false"]::after {
content: var(--accordion-icon-collapsed, " +");
}`;
css.setAttribute('data-accordion-group-css', '');
document.head.append(css);
}
/**
* Create the event handler
*/
createHandler () {
return (event) => {
// Only run on accordion triggers
let trigger = event.target.closest('[trigger]');
if (!trigger) return;
let btn = trigger.firstElementChild;
// Get the content associated with the accordion
let content = trigger.nextElementSibling;
if (!content) return;
// If the content is expanded, hide it
// Otherwise, show it
if (btn.getAttribute('aria-expanded') === 'true') {
btn.setAttribute('aria-expanded', false);
content.setAttribute('hidden', '');
} else {
btn.setAttribute('aria-expanded', true);
content.removeAttribute('hidden');
}
// If not exclusive, all set
if (!this.exclusive) return;
// Otherwise, hide all other open accordions
let openAccordions = this.querySelectorAll('[trigger] > [aria-expanded="true"]');
for (let accordion of openAccordions) {
if (accordion === btn) continue;
accordion.setAttribute('aria-expanded', false);
accordion.parentNode.nextElementSibling.setAttribute('hidden', '');
}
};
}
/**
* Start listening to clicks
*/
connectedCallback () {
this.addEventListener('click', this.handler);
}
/**
* Stop listening to clicks
*/
disconnectedCallback () {
this.removeEventListener('click', this.handler);
}
});