The design pattern for the accordion is inspired by the first example in Sara Soueidan's article entitled: Accordion markup and the accordion keyboard navigation is based on code from W3C accordion example.
Check the accordion online: https://ziizium.github.io/my-webdev-notes/accordion/
Intoduction
An accordion is a graphical control element used for showing or hiding large amounts of content on a Web page. On a normal day, accordions are vertically stacked list of items that can be expanded or stretched to reveal the content associated with them.
Accordion gives control to people when it comes to reading a Web page content. The user can ignore the accordion or read its content by expanding it.
This simple but detailed post is about creating a usable and accessible accordion using HTML, CSS, and a lot of JavaScript (considering how small the accordion is). As stated earlier the accordion has to be accessible, therefore, we have to satisfy the following requirements:
- The contents of the accordion must be readable without CSS.
- The contents of the accordion must be accessible without JavaScript.
- The user should be able to print the contents of the accordion.
In order to satisfy all three requirements mentioned above, we have to build the accordion with accessibility in mind and before every coding decision. We have to keep our users in mind and approach the development in a progressive enhancement manner.
This means we must start with semantic HTML, then we add some CSS that will not render the content of the accordion useless without it and finally we add JavaScript for the true accordion interactivity.
The HTML markup
As stated at the beginning of this post the design pattern for the accordion is inspired by an example from Sara Souiedan's post entitled: Accordion markup. The markup is giving in the image below.
When we convert this to code, users with CSS or JavaScript can access the content, then with JavaScript, we can convert it to the following markup accessible to users with a JavaScript-enabled browser:
The markup is giving in the snippet below:
<header>
<h1 id="h1" style="">Accordion</h1>
</header>
<main>
<article class="accordion">
<h2 class="accordion__title">First title</h2>
<div class="accordion__panel">
<p><!-- Put large text content here --></p>
</div>
</article>
<article class="accordion">
<h2 class="accordion__title">Second title</h2>
<div class="accordion__panel">
<p><!-- Put large text content here --></p>
</div>
</article>
<article class="accordion">
<h2 class="accordion__title">Third title</h2>
<div class="accordion__panel">
<p><!-- Put large text content here --></p>
</div>
</article>
</main>
When you load the file in your browser you'll get something similar to the image below:
This is our baseline experience and browsers with no support for CSS or JavaScript will have access to the accordion content.
The CSS and JavaScript code
Next, we need to add some basic styling to the elements on the page so that we have a better view of what we are working on.
/* CSS reset */
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
/* End of CSS reset */
/**
* Cpsmetics styles just so you can see the
* accordion on screen properly
*/
body {
font-family: "Fira code", "Trebuchet Ms", Verdana, sans-serif;
}
header {
padding: 1em;
margin-bottom: 1em;
}
header > h1 {
text-align: center;
text-transform: uppercase;
letter-spacing: 0.05em;
}
main {
display: block;
width: 100%;
}
@media screen and (min-width: 48em) {
main {
width: 70%;
margin: 0 auto;
}
}
p {
font-family: Georgia, Helvetica, sans-serif;
font-size: 1.2em;
line-height: 1.618;
margin: 0.5em 0;
}
/* End of Cosmetic styles */
In its current state, the accordions are closer to each other and the contents are aligning with the headers, we need to change this. First, we apply some padding to push the content a little bit to the right, we change the background color and at the same time, we take care of overflow so that the content of one accordion won't affect the content of the subsequent accordion.
In the end, we add a margin between the edges of the accordions and some animation using CSS transitions so the accordion content can feel like sliding in and out of view. The next snippet will take care of this.
/**
* The accordion panel is shown by default
* and is hidden when the page loads the
* JavaScript code.
*/
.accordion__panel {
padding: 0 18px;
background-color: #ffffff;
overflow: hidden;
transition: 0.6s ease-in-out;
margin-bottom: 1em;
}
When you reload your browser you will notice minor changes. Let's proceed.
Due to the way accordions work we need to hide the accordion panels before the user can expand or ignore it. We can not hide the panel by adding properties that will hide it directly to the accordion__panel
class and later use JavaScript to remove these properties in order to show it because if we do this any user with JavaScript disabled in their browser will not be able to expand the panel and ultimately loose access to the accordion content.
The better approach is to write a CSS class that will hide the panel and then we can add this class to the accordion panel via JavaScript. Doing this any user who has JavaScript disabled in their browser will have access to the accordion content because JavaScript was unable to hide.
There are several ways to hide stuff in CSS. In our approach, we set the height and opacity of the panel to zero.
/* We hide it with JavaScript */
.accordion__panel.panel-js {
max-height: 0;
opacity: 0;
}
Then we'll have to add this to the panel via JavaScript.
I made the assumption that you will use the format of the accordion HTML markup and the resulting JavaScript code in your projects and you won't like the variable declarations to mess up your codebase, therefore, all the code for our accordion will be placed in an Immediately Invoked Function Expression (IIFE). Doing this all the variables will only live inside the IIFE and won't pollute the global scope.
Create a script
tag or a JavaScript file to save the JavaScript code and create an IIFE syntax as shown below:
(function () {
// All JavaScript for the accordion should be inside this IIFE
})();
Now, we can write code that will hide the panel. The approach is straight forward, we'll grab all the accordion panels and then add the .panel-js
CSS code to each panel via the classList
attribute.
/**
* We hide the accordion panels with JavaScript
*/
let panels = document.getElementsByClassName('accordion__panel');
for (let i = 0; i < panels.length; i++) {
panels[i].classList.add('panel-js');
}
When you save your file and refresh your browser you will realize the panel is now hidden and all you'll see are the accordion titles.
That view is boring, let's change it.
The approach we'll take is similar to how we hid the panels. First, we will grab all the accordion titles and we loop through the resulting NodeList
and then we'll transform the accordion title to a button
which will have a span
element within it that will be the new accordion title. All this is inspired from the example taken from Sara's blog post.
As a refresher and to prevent you from scrolling to the beginning of this blog post, here is the image that we'll implement:
First, we grab all the accordion titles using document.getElementsByClassName
, then we'll loop through the result and perform the following steps:
- Create the
button
andspan
elements. - Create a text node from the accordion titles.
- Append the text node to the newly created
span
elements. - Append the
span
element to the newly createdbutton
element. - Append the
button
to the accordion titles. - Delete the text in the accordion title since we already appended it to the newly created
span
element. - Set the
button
attributes. - Set the accordion panel attributes.
In code:
/**
* We grab the accordion title and create
* the button and span elements. The button
* will serve as the accordion trigger and the
* span element will contain the accordion title.
*
*/
let accordionTitle = document.getElementsByClassName('accordion__title');
for (let i = 0; i < accordionTitle.length; i++) {
// Create the button and span elements
let button = document.createElement('button');
let span = document.createElement('span');
// We create a text node from the accordion title
let textNode = document.createTextNode(accordionTitle[i].innerHTML);
// We append it to the newly created span element
span.appendChild(textNode);
// We append the span element to the newly created
// button element
button.appendChild(span);
// Then we append the button to the accordion title
accordionTitle[i].appendChild(button);
// We delete the text in the accordion title
// since we already grabbed it and appended it
// to the newly created span element.
button.previousSibling.remove();
// Set the button attributes
button.setAttribute('aria-controls', 'myID-' + i);
button.setAttribute('aria-expanded', 'false');
button.setAttribute('class', 'accordion__trigger');
button.setAttribute('id', 'accordion' + i + 'id')
// The next sibling of the accordion title
// is the accordion panel. We need to attach the
// corresponding attributes to it
let nextSibling = accordionTitle[i].nextElementSibling;
if (nextSibling.classList.contains('accordion__panel')) { // just to be sure
// set the attributes
nextSibling.setAttribute('id', 'myID-' + i);
nextSibling.setAttribute('aria-labelled-by', button.getAttribute('id'));
nextSibling.setAttribute('role', 'region');
}
} // End of for() loop
Save and refresh your browser. The titles are now HTML buttons and when you inspect a button with the Developer Tools you'll see the attributes we created.
The buttons are quite small because we have not styled them, let's change that!.
/**
* This removes the inner border in Firefox
* browser when the button recieves focus.
* The selector is take from:
*
* https://snipplr.com/view/16931
*
*/
.accordion__title > button::-moz-focus-inner {
border: none;
}
.accordion__title > button {
color: #444444;
background-color: #dddddd;
padding: 18px;
text-align: left;
width: 100%;
border-style: none;
outline: none;
transition: 0.4s;
}
.accordion__title > button > span {
font-size: 1.5em;
}
/* The .active is dynamically added via JavaScript */
.accordion__title.active > button,
.accordion__title > button:hover {
background-color: #bbbbbb;
}
.accordion__title > button:after {
content: "\02795"; /* plus sign */
font-size: 13px;
color: #777777;
float: right;
margin-left: 5px;
}
/**
* When the accordion is active we change
* the plus sign to the minus sign.
*/
.accordion__title.active > button:after {
content: "\02796"; /* minus sign */
}
Save and refresh your browser. We have a better view!
There is a tiny little problem. When you click the button nothing happens, that is because we have not created two things:
- The CSS code that will allow show us the panel.
- The JavaScript code that will dynamically add and remove this CSS code.
Let's start with the CSS. If you remember from the .panel-js
CSS code, we hid the panel by setting the max_height
and opacity
to zero. Now, we have to do the reverse to reveal the panel and its content.
/**
* When the user toggle to show the accordion
* we increase its height and change the opacity.
*/
.accordion__panel.show {
opacity: 1;
max-height: 500px;
}
The JavaScript to reveal the panel is a little bit tricky. We'll attach an event listener to all accordion titles and perform the following steps:
- Add the
.active
CSS class that we declared earlier when styling the buttons. - Grab the accordion panel.
- Hide or show the panel based on the user interaction.
- Count the accordion title child elements.
- We expect it to be a single button so we get the tag name via its index.
- If the child element is one and in fact a button, we perform the following
- Save the child element in a variable.
- We get its
aria-expanded
value. - If the
aria-expanded
value isfalse
we set it totrue
otherwise we set it tofalse
.
The resulting JavaScript code:
for (let i = 0; i < accordionTitle.length; i++) {
accordionTitle[i].addEventListener("click", function() {
// Add the active class to the accordion title
this.classList.toggle("active");
// grab the accordion panel
let accordionPanel = this.nextElementSibling;
// Hide or show the panel
accordionPanel.classList.toggle("show");
// Just to be safe, the accordion title
// must have a single child element which
// is the button element, therefore, we count
// the child element
let childElementCount = this.childElementCount;
// We get the tag name
let childTagName = this.children[0].tagName;
// Then we check its just a single element and
// it's in fact a button element
if (childElementCount === 1 && childTagName === "BUTTON") {
// If the check passed, then we grab the button
// element which is the only child of the accordion
// title using the childNodes attribute
let accordionButton = this.childNodes[0];
// Grab and switch its aria-expanded value
// based on user interaction
let accordionButtonAttr = accordionButton.getAttribute('aria-expanded');
if (accordionButtonAttr === "false") {
accordionButton.setAttribute('aria-expanded', 'true');
} else {
accordionButton.setAttribute('aria-expanded', 'false');
}
}
});
} // End of for() loop
Save your file and refresh your browser. Now, click the button to reveal or hide the accordion panel and its content.
There you go our accordion is complete! Or is it?
There are two problems in this completed accordion:
- The user can not navigate the accordion with their keyboard
- The user can not print the content of the accordion
The first point is evident when you hit the Tab
key on your keyboard the accordion button does not receive focus.
For the second point, when the user prints the accordion they will only see the accordion title in the printed document. A print preview is shown below in Chrome:
This is quite easy to fix but to enable the keyboard navigation is not straight forward. Let's start with it then we'll fix the printing issue later.
If we want the user to navigate through the accordion with their keyboard we'll have to listen for events specifically on the accordion buttons which have a class titled .accordion__trigger
. When we select all elements with this class name, we'll get a NodeList
in return.
This NodeList
has to be converted to an array. Why? Because when the user navigates through the accordion with their keyboard we must calculate the location of the next accordion using the index location of the current accordion and the number of accordions on the Web page. By this, you should know we are going to need the indexOf
operator to get the location of the current accordion and the length
property which will return the number of accordions on the Web page.
The length
property is available to the NodeList
but the indexOf
is not. Hence, the conversion.
We'll use Array.prototype.slice.call()
method to convert the NodeList
to an array then we'll grab all accordions via their class name .accordion
then loop through the result and perform the following steps:
- Add an event listener to all accordions and we listen for the
keydown
event. - We get the
target
element which is the current element that has received the event. - We get the corresponding key that the user pressed on their keyboard.
- We check if the user is using the
PgUp
orPgDn
keys to navigate the accordion. - To be safe we make sure that the button truly has the
.accordion__trigger
class name then we perform the following steps:- We check if the user is using the arrow keys on their keyboard or if they are using it along with the
Ctrl
key then we perform the following steps:- Get the index of the currently active accordion.
- Check the direction of the user arrow keys, if they are using the down key we set the value to
1
else we set it to-1
. - Get the length of the array of accordion triggers.
- Calculate the location of the next accordion.
- Add a
focus
class to this accordion. - We prevent the default behavior of the buttons.
- Else if the user is using the
Home
andEnd
keys on their keyboard we do the following:- When the user presses the
Home
key we move focus to the first accordion. - When they press the
End
key we move focus to the last accordion. - We prevent the default behavior of the buttons.
- When the user presses the
- We check if the user is using the arrow keys on their keyboard or if they are using it along with the
All these steps converted to code is in the snippet below:
/**
* The querySelectorAll method returns a NodeList
* but we will like to loop through the triggers
* at a later time so that we can add focus styles
* to the accordion title that's why we convert
* the resulting NodelIst into an array which will
* allow us too used Array methods on it.
*/
let accordionTriggers = Array.prototype.slice.call(document.querySelectorAll('.accordion__trigger'));
for (let i = 0; i < accordion.length; i++) {
accordion[i].addEventListener('keydown', function(event) {
let target = event.target;
let key = event.keyCode.toString();
// 33 = Page Up, 34 = Page Down
let ctrlModifier = (event.ctrlKey && key.match(/33|34/));
if (target.classList.contains('accordion__trigger')) {
// Up/ Down arrow and Control + Page Up/ Page Down keyboard operations
// 38 = Up, 40 = Down
if (key.match(/38|40/) || ctrlModifier) {
let index = accordionTriggers.indexOf(target);
let direction = (key.match(/34|40/)) ? 1 : -1;
let length = accordionTriggers.length;
let newIndex = (index + length + direction) % length;
accordionTriggers[newIndex].focus();
event.preventDefault();
}
else if (key.match(/35|36/)) {
// 35 = End, 36 = Home keyboard operations
switch (key) {
// Go to first accordion
case '36':
accordionTriggers[0].focus();
break;
// Go to last accordion
case '35':
accordionTriggers[accordionTriggers.length - 1].focus();
break;
}
event.preventDefault();
}
}
});
}
If you save your file and refresh your browser the keyboard navigation should work but you won't know the currently active accordion. The fix is simple, we have to add a focus style to the parent element of the currently active button (the accordion triggers) which is anh2
element. We remove the focus styles when the accordion is not active.
The CSS focus styles:
.accordion__title.focus {
outline: 2px solid #79adfb;
}
.accordion__title.focus > button {
background-color: #bbbbbb;
}
The resulting JavaScript code:
// These are used to style the accordion when one of the buttons has focus
accordionTriggers.forEach(function (trigger) {
// we add and remove the focus styles from the
// h1 element via the parentElment attibuts
trigger.addEventListener('focus', function (event) {
trigger.parentElement.classList.add('focus');
});
trigger.addEventListener('blur', function (event) {
trigger.parentElement.classList.remove('focus');
});
});
To fix the print issue we have to revert the styles for the accordion panels to its initial state before it was hidden with JavaScript and some few modifications.
The reverted styles have to be placed in a media
query targeting print media.
/**
* Print styles (Just in case your users
* decide to print the accordions content)
*/
@media print {
.accordion__panel.panel-js {
opacity: 1;
max-height: 500px;
}
.accordion__title button {
font-size: 0.7em;
font-weight: bold;
background-color: #ffffff;
}
.accordion__title button:after {
content: ""; /* Delete the plus and minus signs */
}
}
The new print preview in Chrome:
With that, we are done with the accordion. The code is not perfect but it works and you can improve it.
The GitHub repo for this series:
ziizium / my-webdev-notes
Code snippets for series of articles on DEV about my experiments in web development
My WebDev Notes
This repositiory contains code snippets, and links for series of articles on DEV about my experiments in Web development.
List of articles
- My WebDev Notes: CSS Loaders published on the 25th February 2020
- My WebDev Notes: Filter table published on the 1st April 2020
- MyWebDev Notes: Center page elements with CSS Grid published on the 3rd of April 2020
- My WebDev Notes: Photo gallery with CSS Grid published on the 7th of April 2020
- My WebDev Notes: Fullscreen overlay navigation published on the 13th of April 2020
- My WebDev Notes: A simple and accessible accordion published on 28th of April 2020
- My WebDev Notes: How to create a tooltip with HTML and CSS published on 3rd February 2021
- How to create a modal published on 22nd June 2021
Have fun!
Top comments (6)
@ziizium
Have you thought about adding a link to a working version of your example?
You could host your demos quite easily using GitHub pages.
The article has been updated with a link to a working version of the accordion (check the blockquote at the beginning of this article).
The URL is: ziizium.github.io/my-webdev-notes/...
Thank you once again.
How about adding a link to the source code from the demo?
Also you might consider comparing and contrasting with the details HTML tag
On it. I'll let you know when it's done.
Yeah, I read about the details tag on Go Make Things and based on the current data on Can I Use it has 93.12%. I have not read about it from the MDN link in your comment (I will now).
Thank you, I really appreciate your comment (s).
@theoarmour a link to the source code is now on the demo page.
Thank you, and I am sorry it took so long.
Yeah, I should have done that. My bad.
I wrote an article about a simple TO-DO application which is live on GitHub pages and a working example for the article CSS Loaders is live on codesandbox.
I'll host a working example for this article on GitHub pages. I'll let you know when it's live.
Thank you very much for pointing it out. I appreciate it.
Edited 30th April 2020: The new URL location of the TO-DO app