An accordion component is used to:
Alternatives to using an accordion component:
There are two types of accordions:
Accordions with one single expandable item are displayed with a chevron on the right-hand side.
By default, the single expandable item is collapsed.
The single expandable items has a label that tells what content is in the item. Do not start the label with a verb that describes the collapsing or expanding. This is not necessary because the item is already in an accordion. For example, if the item is a list of opening hours deze week, don’t use a label “Show opening hours this week” but use “Opening hours this week” instead.
Accordions with multiple expandable items are displayed in a list of items with labels with a plus or a minus icon on the left-hand side of each item.
By default, all the expandable items are collapsed. Multiple items can be expanded at the same time.
The expandable items each have a label that tells what content is in the item. Do not start the label with a verb that describes the collapsing or expanding. This is not necessary because the item is already in an accordion. For example, if an item is about transportation by train, don’t use a label “Show transportation by train” but use “Transportation by train” instead.
Create a new accordion object by running:
new Accordion(element);
Where element contains a button with:
And a collapsible element with:
By default, the accordion will initiate automatically
and hide or show the content
according to the aria-expanded attribute.
If the hash in the URL contains a matching ID,
this element will also be expanded.
For instance:
<div class="accordion">
<h3>
<button aria-controls="accordion--content-ID"
aria-expanded="false"
class="accordion--button">
single accordion
</button>
</h3>
<div class="accordion--content" id="accordion--content-ID">
...
</div>
</div>
Option | Type | Default | Description |
---|---|---|---|
expand |
function |
function(button, content) |
Function triggered after the expanded class is added, the ‘hidden’ attribute is removed from the content and aria-hidden is set to false. |
collapse |
function |
function(button, content) |
Function triggered after the expanded class is removed and aria-hidden is set to true. By default, the ‘hidden’ attribute is set in the transitionEnd function |
transitionEnd |
function |
function(event) |
Triggered for each transitionEnd event, use this to add the ‘hidden’ attribute after the content has been transitioned out of view. |
resizeEvent |
function |
function(event, expandedContent) |
ExpandedContent is an array containing all expanded elements. Use this to trigger the ‘expand’ function on window.resize |
init |
Boolean |
true |
Set to false if you want to manually initiate the accordion object (object.init()) |
buttonSelector |
String |
'button.accordion--button' |
QuerySelector to identify the accordion trigger button |
accordionExpandedClass |
String |
'accordion--expanded' |
Determine which class is added to the expanded content. |
Function | Description |
---|---|
init() |
Manually initiate the accordion, this will expand or collapse all content according to the aria-expanded state |
closeAll() |
Close all collapsible content in this accordion |
openAll() |
Open all collapsible content in this accordion |
<ul class="accordion">
{% for item in items %}
<li>
<h3>
<button class="accordion--button"
aria-expanded="false"
aria-controls="accordion--multiple--content--{{ loop.index }}">
{{ item.buttonText }}
</button>
</h3>
<div class="accordion--content"
id="accordion--multiple--content--{{ loop.index }}">
{{ item.content }}
</div>
</li>
{% endfor %}
</ul>
<ul class="accordion">
<li>
<h3>
<button class="accordion--button" aria-expanded="false" aria-controls="accordion--multiple--content--1">
First item
</button>
</h3>
<div class="accordion--content" id="accordion--multiple--content--1">
<p>Multiple item accordions are contained in a list element.</p>
<p>They are indicated with a 'plus' or 'minus' icon</p>
<p>Below is some nonsens text.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi culpa dolorum enim molestiae molestias nemo nulla quas sed, temporibus voluptatem!</p>
</div>
</li>
<li>
<h3>
<button class="accordion--button" aria-expanded="false" aria-controls="accordion--multiple--content--2">
Second item
</button>
</h3>
<div class="accordion--content" id="accordion--multiple--content--2">
<p>Single item accordions are indicated by a chevron.</p>
<p>Below is some nonsens text.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi culpa dolorum enim molestiae molestias nemo nulla quas sed, temporibus voluptatem!</p>
</div>
</li>
<li>
<h3>
<button class="accordion--button" aria-expanded="false" aria-controls="accordion--multiple--content--3">
Third item
</button>
</h3>
<div class="accordion--content" id="accordion--multiple--content--3">
<p>Single item accordions are indicated by a chevron.</p>
<p>Below is some nonsens text.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi culpa dolorum enim molestiae molestias nemo nulla quas sed, temporibus voluptatem!</p>
</div>
</li>
<li>
<h3>
<button class="accordion--button" aria-expanded="false" aria-controls="accordion--multiple--content--4">
Fourth item
</button>
</h3>
<div class="accordion--content" id="accordion--multiple--content--4">
<p>Single item accordions are indicated by a chevron.</p>
<p>Below is some nonsens text.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi culpa dolorum enim molestiae molestias nemo nulla quas sed, temporibus voluptatem!</p>
</div>
</li>
</ul>
{
"items": [
{
"buttonText": "First item",
"content": "<p>Multiple item accordions are contained in a list element.</p><p>They are indicated with a 'plus' or 'minus' icon</p><p>Below is some nonsens text.</p><p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi culpa dolorum enim molestiae molestias nemo nulla quas sed, temporibus voluptatem!</p>"
},
{
"buttonText": "Second item",
"content": "<p>Single item accordions are indicated by a chevron.</p><p>Below is some nonsens text.</p><p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi culpa dolorum enim molestiae molestias nemo nulla quas sed, temporibus voluptatem!</p>"
},
{
"buttonText": "Third item",
"content": "<p>Single item accordions are indicated by a chevron.</p><p>Below is some nonsens text.</p><p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi culpa dolorum enim molestiae molestias nemo nulla quas sed, temporibus voluptatem!</p>"
},
{
"buttonText": "Fourth item",
"content": "<p>Single item accordions are indicated by a chevron.</p><p>Below is some nonsens text.</p><p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi culpa dolorum enim molestiae molestias nemo nulla quas sed, temporibus voluptatem!</p>"
}
]
}
.accordion {
.accordion--button {
@include icon(chevron-down);
position: relative;
padding-right: 1.6rem;
padding-left: 0;
color: inherit;
text-align: left;
&::before {
position: absolute;
right: 0;
transition: transform .2s ease-in-out;
font-size: 1.4rem;
line-height: 1.1;
}
&[aria-expanded=true] {
&::before {
transform: scaleY(-1);
}
}
}
.accordion--content {
transition: max-height .5s ease-in-out;
overflow: hidden;
p {
line-height: 1.75;
}
}
}
ul.accordion,
dl.accordion {
.accordion--button {
width: 100%;
padding: 0;
overflow: visible;
&::before {
position: relative;
right: auto;
left: 0;
min-width: 1.6rem;
transition: transform .2s ease-in-out;
font-size: 1.2rem;
line-height: 1.2;
text-align: left;
}
&[aria-expanded=true] {
@include icon(minus);
&::before {
transform: none;
}
}
&[aria-expanded=false] {
@include icon(plus);
}
}
.accordion--content {
margin-left: 1.6rem;
}
}
ul.accordion {
margin: 0;
li {
list-style: none;
}
}
dl.accordion {
dd {
padding: 0;
}
}
'use strict';
(function () {
if (!Accordion) { // eslint-disable-line no-undef
return;
}
const selected = document.querySelectorAll('.accordion');
for (let i = selected.length; i--;) {
new Accordion(selected[i]); // eslint-disable-line no-undef
}
})();
'use strict';
/* global define, module */
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(factory);
}
else {
if (typeof exports === 'object') {
module.exports = factory();
}
else {
root.Accordion = factory();
}
}
}(this || window, function () {
return function (elem, options) {
let expandedContent = [];
/**
* Override default options with options param.
*/
options = (() => {
const defaults = {
expand: (button, content) => {
content.style.maxHeight = `${content.scrollHeight}px`;
},
collapse: (button, content) => {
content.style.maxHeight = 0;
},
transitionEnd: (e) => {
if (e.propertyName !== 'max-height') {
return;
}
if (!e.currentTarget.classList.contains('accordion--expanded')) {
e.currentTarget.setAttribute('hidden', 'hidden');
}
},
resizeEvent: (e, expandedContent) => {
for (let i = expandedContent.length; i--;) {
options.expand(null, expandedContent[i]);
}
},
init: true,
buttonSelector: 'button.accordion--button',
accordionExpandedClass: 'accordion--expanded'
};
const keys = Object.keys(defaults);
options = options || {};
for (let i = keys.length; i--;) {
options[keys[i]] = options[keys[i]] || defaults[keys[i]];
}
return options;
})();
const buttons = elem.querySelectorAll(options.buttonSelector);
/**
* Toggle aria-expanded attributes and trigger visibility Change function.
*
* @param {event} e The triggered event.
*/
const toggle = (e) => {
e.preventDefault();
const button = e.currentTarget;
button.setAttribute('aria-expanded', button.getAttribute('aria-expanded') === 'true' ? 'false' : 'true');
setVisibility(button);
};
/**
* Handle keyboard input: arrows, home & end.
*
* @param {event} e The triggered event.
*/
const keyDown = (e) => {
const keyCode = e.keyCode || e.which;
let current = (() => {
for (let i = buttons.length; i--;) {
if (buttons[i] === e.currentTarget) {
return i;
}
}
})();
switch (keyCode) {
case 40: // arrow down
e.preventDefault();
if (current === buttons.length - 1) {
current = -1;
}
buttons[current + 1].focus();
break;
case 38: // arrow up
e.preventDefault();
if (current === 0) {
current = buttons.length;
}
buttons[current - 1].focus();
break;
case 36: // home
e.preventDefault();
buttons[0].focus();
break;
case 35: // end
e.preventDefault();
buttons[buttons.length - 1].focus();
break;
}
};
/**
* Add events to buttons.
* Listen for animationEnd on accordionContent.
*/
const addEvents = () => {
const onResize = (e) => {
options.resizeEvent(e, expandedContent);
};
for (let i = buttons.length; i--;) {
const button = buttons[i];
button.addEventListener('click', toggle);
button.addEventListener('keydown', keyDown);
const accordionContent = elem.querySelector(`#${button.getAttribute('aria-controls')}`);
accordionContent.addEventListener('transitionend', options.transitionEnd);
if (options.resizeEvent) {
window.addEventListener('resize', onResize);
}
}
window.addEventListener('hashchange', hashEvent);
};
/**
* Hide or show the accordion content.
*
* @param {Object} button The accordion button.
* @param {boolean|false} isInitial True if this is the first run
* triggered by init().
*/
const setVisibility = (button, isInitial) => {
const accordionContent = elem.querySelector(`#${button.getAttribute('aria-controls')}`);
if (!accordionContent) {
return;
}
if (button.getAttribute('aria-expanded') === 'true') {
accordionContent.classList.add(options.accordionExpandedClass);
accordionContent.setAttribute('aria-hidden', 'false');
accordionContent.removeAttribute('hidden');
expandedContent.push(accordionContent);
options.expand(button, accordionContent);
}
else {
accordionContent.classList.remove(options.accordionExpandedClass);
accordionContent.setAttribute('aria-hidden', 'true');
if (isInitial) {
accordionContent.setAttribute('hidden', 'hidden');
}
expandedContent.filter(content => content !== accordionContent);
options.collapse(button, accordionContent);
}
};
/**
* Set all attributes and toggle visibility accordingly.
*/
const setInitial = () => {
for (let i = buttons.length; i--;) {
setVisibility(buttons[i], true);
}
};
const close = (button) => {
button.setAttribute('aria-expanded', 'false');
setVisibility(button);
};
/**
* Closes all accordion items.
*/
const closeAll = () => {
for (let i = buttons.length; i--;) {
close(buttons[i]);
}
};
const open = (button) => {
button.setAttribute('aria-expanded', 'true');
setVisibility(button);
};
/**
* Opens all accordion items.
*/
const openAll = () => {
for (let i = buttons.length; i--;) {
open(buttons[i]);
}
};
/**
* Open the accordion-content related to the location hash.
*/
const hashEvent = () => {
let hash = window.location.hash.replace('#', '');
if (!hash) {
return;
}
const trigger = elem.querySelector(`[aria-controls=${hash}]`);
if (trigger) {
open(trigger);
trigger.focus();
}
};
/**
* Enable accordion functionality.
*/
const init = () => {
setInitial();
addEvents();
hashEvent();
};
if (options.init !== false) {
init();
}
return {init, closeAll, openAll};
};
}));