My WebDev Notes: A simple and accessible accordion
Tags: html, css, javascript
Published: 2020-04-28 10:22:39

---
title: My WebDev Notes: A simple and accessible accordion
published: true
description: How to create a simple and accessible accordion
tags: html, css, javascript
series: My WebDev Notes
---

> <b>The design pattern for the accordion is inspired by the first example in [Sara Soueidan's article entitled: Accordion markup](https://www.sarasoueidan.com/blog/accordion-markup/) and the accordion keyboard navigation is based on code from [W3C accordion example](https://www.w3.org/TR/wai-aria-practices/examples/accordion/accordion.html)</b>.

> <b>Check the accordion online: https://ziizium.github.io/my-webdev-notes/accordion/</b>

##__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](https://www.sarasoueidan.com/blog/accordion-markup/). The markup is giving in the image below.

![An HTML code for a presumed accordion](https://dev-to-uploads.s3.amazonaws.com/i/ptzufdzr2i5kvqaw3nkg.png)

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:

![An HTML code for a presumed accordion](https://dev-to-uploads.s3.amazonaws.com/i/u15v8ddqm1cd07n4u13r.png)

The markup is giving in the snippet below:

```html
    <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:

![HTML view of the accordion](https://dev-to-uploads.s3.amazonaws.com/i/ywzyiwu41rn3o7bxmtl3.JPG)

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
 /* 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.

```css
/**
 * 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_.

```css
/* 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:

```js
(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.

```js
/**
 * 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.

![Acoordion title](https://dev-to-uploads.s3.amazonaws.com/i/kswmx796nourol11pcyn.png)

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:

![An HTML code for a presumed accordion](https://dev-to-uploads.s3.amazonaws.com/i/u15v8ddqm1cd07n4u13r.png)

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` and `span` 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 created `button` 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:

```js
/**
 * 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!.

```css
/**
 * 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!

![A better view of the accordion](https://dev-to-uploads.s3.amazonaws.com/i/fzqzqivus4vwyli3ijtd.png)

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.

```css
/**
 * 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 is `false` we set it to `true` otherwise we set it to `false`.

The resulting JavaScript code:

```js
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.

![Completed accordion image](https://dev-to-uploads.s3.amazonaws.com/i/zl68deznyttxck8lbcqa.png)

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:

![Print preview in Chrome](https://dev-to-uploads.s3.amazonaws.com/i/aoqwccipmtjtuhohxju4.JPG)

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` or `PgDn` 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` and `End` 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.

All these steps converted to code is in the snippet below:

```js
/**
 * 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 an`h2` element. We remove the focus styles when the accordion is not active.

The CSS focus styles:

```css
.accordion__title.focus {
	outline: 2px solid #79adfb;
}

.accordion__title.focus > button {
	background-color: #bbbbbb;
}
```

The resulting JavaScript code:

```js
// 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');
  	});

});
```

![Final accordion image](https://dev-to-uploads.s3.amazonaws.com/i/dfkvpc031ip5ayn115sy.JPG)

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.

```css
/**
* 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:

![Print preview in Chrome](https://dev-to-uploads.s3.amazonaws.com/i/o0pnvhez1ulge6lgzx34.JPG)

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:

{% github ziizium/my-webdev-notes %}

_Have fun!_