
Posted on • Edited on • Originally published ata11ywithlindsey.com
JavaScript and Accessibility: Accordions
Originally posted onwww.a11ywithlindsey.com.
When I first wrote my post aboutJavaScript and Accessibility, I promised I would make it a series. I've decided to use mypatreon to have votes on what my next blog post is. This topic won, and I'm finally getting more time to write about JavaScript!
So this topic I am going to go into a deep dive on how to make accordions accessible! Our focus is:
- Accessing the accordion with a keyboard
- Screen reader support
HTML Structure
I did a few pieces of research about the HTML structure. I read thea11y project's link toScott O'Hara's Accordion code. I also readDon's take about aria-controls - TL;DR he thinks they're poop. I couldn't escape reading theWAI-ARIA Accordion example as they set a lot of the standards. My hope is with all the information about what's ideal, I can help talk through why everything is important here. It's easy to get overwhelmed, and I'm here to help!
So if you read my post3 Simple Tips to Improve Keyboard Accessibility, you may recall my love for semantic HTML.
If you need JavaScript for accessibility, semantic HTML makes your job significantly easier.
Many of the examples I found use semantic button elements for the accordion headings. Then the examples used div tags as siblings. Below is how my code starts:
Adding the ARIA attributes
I wrote that ARIA is not a replacement for semantic HTML in aprevious post. New HTML features that come out are replacing ARIA all the time. In an ideal world, I would use thedetails element. Unfortunately, according to theBrowser Compatibility Section, there is no support for Edge and IE11. Until browser support improves, I'll be sticking to the "old fashioned" way of doing it. I'll be adding ARIA for the context we need. I'm looking forward to seeing the compatibility expand to Edge!
First, I am going to add somearia-hidden
attributes to the div to indicate thestate of the accordion content. If the collapsed element isclosed, we want to hide that content from the screen reader. Can you imagine how annoying it would be to read through the content you are not interested in?
- <div>+ <div aria-hidden="true">......- <div>+ <div aria-hidden="true">......- <div>+ <div aria-hidden="true">
The next thing we do is ensure that we have anaria-expanded
attribute to the button. When we are on the button, it tells us if something is expanded or collapsed.
- <button>+ <button aria-expanded="false">......- <button>+ <button aria-expanded="false">......- <button>+ <button aria-expanded="false">
When it comes to ARIA for me, less is more. I am going to leave it at that and use JavaScript in a future section to toggle the states of the ARIA attributes.
Adding Some Styling
I'm not going to focus too much on the CSS specifics. If you need a CSS resource, Ali Spittel's postCSS: From Zero to Hero and Emma Wedekind'sCSS Specificity post are great.
First, I add classes to the divs and the buttons for good measure.
- <button aria-expanded="false">+ <button aria-expanded="false"> Section 1 </button>- <div aria-hidden="true">+ <div aria-hidden="true">
Then I add a bunch of styling to the buttons. I wrote this CodePen with SCSS.
(Quick note: for the triangles on the iframe, I used theCSS Triangle article from CSS tricks.)
I want to point outexplicitly this code:
.accordion{// previous styling&__button.expanded{background:$purple;color:$lavendar;}}
I want to specify what the button looks like when it was open. I like how it draws your eye and attention to the open section. Now that I see what they generally look like, I am going to add the styling to collapse them. Additionally, I'm adding some open styling.
&__section { border-left: 1px solid $purple; border-right: 1px solid $purple; padding: 1rem; background: $lavendar;+ max-height: 0vh;+ overflow: hidden;+ padding: 0; }+ &__section.open {+ max-height: 100vh;+ overflow: auto;+ padding: 1.25em;+ visibility: visible;+ }
Finally, let's add some focus and hover styling for the buttons:
$purple: #6505cc;+ $dark-purple: #310363; $lavendar: #eedbff;
&__button { position: relative; display: block; padding: 0.5rem 1rem; width: 100%; text-align: left; border: none; color: $purple; font-size: 1rem; background: $lavendar;+ &:focus,+ &:hover {+ background: $dark-purple;+ color: $lavendar;++ &::after {+ border-top-color: $lavendar;+ }+ }
A quick note: you could likely add styling by adding.accordion__button[aria-expanded="true"]
or.accordion__section[aria-hidden="false"]
. However, it is my personal preference using classes for styling and not attributes. Different strokes for different folks!
JavaScript toggling
Let's now get to the fun part of toggling the accordion in an accessible way. First, I want to grab all the.section__button
elements.
constaccordionButtons=document.querySelectorAll('.accordion__button')
Then I want to step through every element of the HTML collection that JavaScript returns.
accordionButtons.forEach(button=>console.log(button))// returns <button aria-expanded="false">// Section 1// </button>// <button aria-expanded="false">// Section 2// </button>// <button aria-expanded="false">// Section 3// </button>
Then for each of those items, I want to toggle the class for the opening and closing for visual styling purposes. If you remember the.open
and.expanded
classes that we added before, here is where we toggle them. I am going to use the number in the ids that match up with each other to get the corresponding section for that button.
accordionButtons.forEach(button=>{// This gets the number for the class.// e.g. would be "1"constnumber=button.getAttribute('id').split('-').pop()// This gets the matching ID. e.g. the// section that is underneath the buttonconstassociatedSection=document.getElementById(`accordion-section-${number}`)})
Now we have the current valuebutton
in the callback and the associated section. Now we can get to toggling classes!
button.addEventListener('click',()=>{button.classList.toggle('expanded')associatedSection.classList.toggle('open')})
Toggling classes is not all we want to do. We also want to toggle the aria attributes. From the previous section, aria attributes communicatestate to screen readers. Changing the classes shows what happened to a visual user, not to a screen reader. Next, I check if the button contains the class in one of those elements. If it does, I'll swap the state for thearia-hidden
andaria-expanded
.
button.addEventListener('click', () => { button.classList.toggle('expanded') associatedSection.classList.toggle('open')+ if (button.classList.contains('expanded')) {+ console.log('open?')+ }})
The conditional fires after we set the classes, and if the class has expanded, it is open! So this is where we want to use the states and communicate it's open.
button.addEventListener('click',()=>{button.classList.toggle('expanded')associatedSection.classList.toggle('open')if(button.classList.contains('expanded')){button.setAttribute('aria-expanded',true)associatedSection.setAttribute('aria-hidden',false)}else{button.setAttribute('aria-expanded',false)associatedSection.setAttribute('aria-hidden',true)}})
Now we can open and close the accordion with the spacebar or the enter key!
When I go through the accordions headers without opening them, they do not read them in the section. That's a good thing! When I open it, I'm able to read it.
Progressive Enhancement
Now, I know how much we all rely on JavaScript loading, particularly with all the frameworks we use. Now that we know the functionality, let's refactor the code a bit. The goal is to ensure anyone can access the accordion if JavaScript is not enabled or the user has connectivity issues.
My final touch is to
- Keep all the accordion sections open by default (Adding an
.open
class to the HTML sections) - Remove the 'open' class once the JavaScript loads.
- Add all the aria attributes with JavaScript and remove that from the HTML
I want to removearia-expanded="false"
andaria-hidden="true"
from my buttons and sections, respectively. I also want to add theopen
class to the html, so it's visually open by default.
- <button aria-expanded="false">+ <button> Section 1 </button>- <div aria-hidden="true">+ <div>
I want to set those attributes and remove that class in the forEach loop ofaccordionButtons
.
accordionButtons.forEach(button => {+ button.setAttribute('aria-expanded', false); const expanded = button.getAttribute('aria-expanded');
Then I want to create anaccordionsSections
variable and do two things:
- set the
aria-hidden
attribute - remove the
.open
class.
constaccordionSections=document.querySelectorAll('.accordion__section');accordionSections.forEach(section=>{section.setAttribute('aria-hidden',true)section.classList.remove('open')})
We're done! Remember, we haven't removed any of the other code or event listeners. We are just adding all those attributes in with JavaScript.
Conclusion
What did you think of this post? Did it help you? Are you excited for the<details>
element? Let me know onTwitter what you think! Also, I now have apatreon! If you like my work, consider becoming a patron. You’ll be able to vote on future blog posts if you make a $5 pledge or higher! Cheers! Have a great week!
Top comments(12)

- LocationBaltimore, MD
- WorkEngineering Manager
- Joined
However, it is my personal preference using classes for styling and not attributes.
One of the wins of using ARIA attributes to style interactive components like this is that the component willnever workwithout the ARIA changes baked in. This may not matter in very small teams or teams where there is across-the-board knowledge about accessibility, but for all other cases, I would highly recommend using the ARIA attributes in the CSS, too.
This was a great post, btw!

- Joined
True! I just never forget, but I definitely like the idea of forcing people to deal with it 😃

- LocationBritain, Europe
- PronounsHe/Him
- WorkSenior Web Developer at bloc-digital
- Joined
Great post, as always, Lindsey 🙂

- LocationIreland
- EducationBSc Hons Multimedia Programming & Design
- WorkTechnical Lead at Workhuman
- Joined
Great post, thanks for sharing! I really like the code snippets that show what's changed.

- Joined
Really glad it helps you!
Going things step by step to highlight what changes fixes which issues really helps me learn why I am doing the thing 😁

- LocationNew York City
- WorkFrontend Developer at InVision
- Joined
Such a great post! I'm thinking of spinning up an example via a React component and seeing how it can be implemented there :)

- LocationDelhi, India
- WorkFullstack Web Developer at Codeword Tech
- Joined
really nice post, Is usingdetail
element for accordation do any harm for accessibility or it's less used just because of low browser support

- Joined
Part of accessibility is being robust. In my opinion, this means support of browsers AND assistive devices.
Highly recommend giving this a read!developer.mozilla.org/en-US/docs/W...

- Joined
It's the<details>
tag. It doesn't have full support which is why I went through this, but I included the reference in the "Adding the ARIA attributes" section!
For further actions, you may consider blocking this person and/orreporting abuse