An accordion component is used to:
Alternatives to using an accordion component:
There are two types of accordions:
Adding the class ‘dropdown’ will add the following behaviour: when you click on an element that isn’t a dropdown, all dropdowns will be closed. This behaviour is used in the case of an application that doesn’t refresh when clicking a button or link.
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.
Accordions that should be opened with a link, should have the .accordion--link
class with buttons that have an .accordion--link--button
class.
Make sure the ID is unique and the same in the aria-controls of the button.
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 |
<div class="accordion dropdown">
<{{ headingLevel|default('h3') }}>
<button aria-controls="accordion--single--content--1" aria-expanded="false" class="accordion--button">{{ buttonText }}</button>
</{{ headingLevel|default('h3') }}>
<div id="accordion--single--content--1" class="accordion--content">
{{ content }}
</div>
</div>
<div class="accordion dropdown">
<h3>
<button aria-controls="accordion--single--content--1" aria-expanded="false" class="accordion--button">single accordion</button>
</h3>
<div id="accordion--single--content--1" class="accordion--content">
<p>Single item accordions are indicated by a chevron.</p>
<p>Below is some nonsens text.</p>
<ul>
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi culpa dolorum enim molestiae molestias nemo nulla quas sed, temporibus voluptatem!</p>
</div>
</div>
{
"buttonText": "single accordion",
"content": "<p>Single item accordions are indicated by a chevron.</p><p>Below is some nonsens text.</p><ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul><p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Animi culpa dolorum enim molestiae molestias nemo nulla quas sed, temporibus voluptatem!</p>"
}
.accordion {
position: relative;
z-index: 1;
.accordion--button {
@include extra-large-text;
@include semi-bold-text;
@include icon(chevron-down);
position: relative;
padding-right: 1.6rem;
color: inherit;
line-height: 1.3rem;
text-align: left;
&::before {
position: absolute;
right: 0;
transition: transform .2s ease-out-in-out;
font-size: 1.4rem;
line-height: 1.2;
}
&[aria-expanded=true] {
&::before {
transform: scaleY(-1);
}
}
}
.accordion--content {
max-height: 0;
transition: max-height .75s ease-out;
overflow: hidden;
&.accordion--expanded {
max-height: 9999px;
}
&:focus,
&:focus-within {
overflow: unset; // The overflow is needed for the accordion animation only
}
p {
line-height: 1.75;
}
}
.accordion {
z-index: 2;
}
}
ul.accordion,
dl.accordion {
&:not(.timeline) {
& > li {
@include theme('border-color', 'color-primary--lighten-4', 'accordion-border-color');
border-bottom: 2px solid;
}
}
li {
h3 {
margin: 1rem 0;
}
}
.accordion--button {
display: flex;
align-items: center;
width: 100%;
padding: 0;
overflow: visible;
&::before {
@include theme('background-color', 'color-tertiary-pastel', 'button-secondary-background');
position: relative;
right: auto;
left: 0;
min-width: 1.8rem;
min-height: 1.8rem;
margin: 0 .75rem 0 0;
padding: .13rem .25rem;
transition: transform .2s ease-out-in-out;
font-size: 1.2rem;
text-align: left;
}
&[aria-expanded=true] {
@include icon(minus);
&::before {
transform: none;
line-height: 1.2;
}
}
&[aria-expanded=false] {
@include icon(plus);
&::before {
line-height: 1.2;
}
}
}
}
ul.accordion {
margin: 0;
> li {
list-style: none;
}
}
dl.accordion {
dd {
padding: 0;
}
}
.accordion--link {
position: relative;
z-index: 1;
.accordion--link--button {
@include remove-button;
@include theme('color', 'color-tertiary', 'link-color');
@include link-background('color-primary', 'color-primary', 'link-underline-color', 'link-hover-background');
@include semi-bold-text;
width: auto;
line-height: 120%;
vertical-align: middle;
&::before {
display: none;
}
i {
display: inline-block;
margin-left: 5px;
transform: rotate(0deg);
transform-origin: center center;
transition: .3s;
vertical-align: middle;
}
&[aria-expanded=true] {
i {
transform: rotate(180deg);
}
}
}
.accordion--link--content {
max-height: 0;
transition: max-height .75s ease-out;
overflow: hidden;
&.accordion--expanded {
max-height: 9999px;
}
&:focus,
&:focus-within {
overflow: unset; // The overflow is needed for the accordion animation only
}
p {
line-height: 1.75;
}
}
.accordion {
z-index: 2;
}
}
'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
}
const selectedMore = document.querySelectorAll('.accordion--link');
for (let x = selectedMore.length; x--;) {
new Accordion(selectedMore[x]); // eslint-disable-line no-undef
}
})();
'use strict';
(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',
linkSelector: 'button.accordion--link--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;
})();
// Check if there's a More info link
let buttons;
if (elem.querySelectorAll(options.linkSelector).length === 0) {
buttons = elem.querySelectorAll(options.buttonSelector);
}
else {
buttons = elem.querySelectorAll(options.linkSelector);
}
/**
* 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')}`);
const accordionContentImage = elem.querySelector(`#${button.getAttribute('data-controls-img')}`);
accordionContent.addEventListener('transitionend', options.transitionEnd);
if (accordionContentImage) {
accordionContentImage.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')}`);
const accordionContentImage = elem.querySelector(`#${button.getAttribute('data-controls-img')}`);
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);
if (accordionContentImage) {
accordionContentImage.classList.add(options.accordionExpandedClass);
accordionContentImage.setAttribute('aria-hidden', 'false');
accordionContentImage.removeAttribute('hidden');
expandedContent.push(accordionContentImage);
options.expand(button, accordionContentImage);
}
options.expand(button, accordionContent);
}
else {
accordionContent.classList.remove(options.accordionExpandedClass);
accordionContent.setAttribute('aria-hidden', 'true');
if (isInitial) {
accordionContent.setAttribute('hidden', 'hidden');
}
if (accordionContentImage) {
accordionContentImage.classList.remove(options.accordionExpandedClass);
accordionContentImage.setAttribute('aria-hidden', 'true');
if (isInitial) {
accordionContentImage.setAttribute('hidden', 'hidden');
}
expandedContent.filter(content => content !== accordionContentImage);
options.collapse(button, accordionContentImage);
}
expandedContent.filter(content => content !== accordionContent);
options.collapse(button, accordionContent);
}
};
/**
* Hide dropdowns when clicking outside the element.
*
* @param {Object} event The js event.
*/
window.onclick = function (event) {
if (!event.target.matches('.dropdown button, .opening-hours *, .dropdown--link button')) {
var dropdowns = elem.getElementsByClassName('dropdown');
/* eslint-disable */
for (var i = 0; i < dropdowns.length; i++) {
var button = dropdowns[i].querySelector('button');
if (button.getAttribute('aria-expanded') === 'false') {
return;
}
const accordionContent = dropdowns[i].querySelector(`#${button.getAttribute('aria-controls')}`);
const accordionContentImage = dropdowns[i].querySelector(`#${button.getAttribute('data-controls-img')}`);
if (!accordionContent) {
return;
}
accordionContent.classList.remove(options.accordionExpandedClass);
accordionContent.setAttribute('aria-hidden', 'true');
accordionContent.setAttribute('hidden', 'hidden');
expandedContent.filter(content => content !== accordionContent);
options.collapse(button, accordionContent);
button.setAttribute('aria-expanded', 'false');
if (accordionContentImage) {
accordionContentImage.classList.remove(options.accordionExpandedClass);
accordionContentImage.setAttribute('aria-hidden', 'true');
accordionContentImage.setAttribute('hidden', 'hidden');
expandedContent.filter(content => content !== accordionContentImage);
options.collapse(button, accordionContentImage);
}
}
var dropdownlinks = elem.getElementsByClassName('dropdown--link');
for (var i = 0; i < dropdownlinks.length; i++) {
var link = dropdowns[i].querySelector('button.accordion--link--button');
if (link.getAttribute('aria-expanded') === 'false') {
return;
}
const accordionContent = dropdowns[i].querySelector(`#${link.getAttribute('aria-controls')}`);
const accordionContentImage = dropdowns[i].querySelector(`#${link.getAttribute('data-controls-img')}`);
if (!accordionContent) {
return;
}
accordionContent.classList.remove(options.accordionExpandedClass);
accordionContent.setAttribute('aria-hidden', 'true');
accordionContent.setAttribute('hidden', 'hidden');
expandedContent.filter(content => content !== accordionContent);
options.collapse(link, accordionContent);
link.setAttribute('aria-expanded', 'false');
if (accordionContentImage) {
accordionContentImage.classList.remove(options.accordionExpandedClass);
accordionContentImage.setAttribute('aria-hidden', 'true');
accordionContentImage.setAttribute('hidden', 'hidden');
expandedContent.filter(content => content !== accordionContentImage);
options.collapse(link, accordionContentImage);
}
}
/* eslint-enable */
}
};
/**
* 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;
}
try {
const trigger = elem.querySelector(`[aria-controls=${hash}]`);
if (trigger) {
open(trigger);
trigger.focus();
}
}
catch (e) { }
};
/**
* Enable accordion functionality.
*/
const init = () => {
setInitial();
addEvents();
hashEvent();
};
if (options.init !== false) {
init();
}
return {init, closeAll, openAll};
};
}));