Skip to main content Accessibility Feedback

<accordion-group>

Create progressively enhanced accordion groups.

Source Code

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

});

Find this useful? You can support my work by purchasing an annual membership.