How to Code an Accessible JavaScript Accordion Component
In this tutorial, we’ll be creating an accessible accordion block component using vanilla JavaScript, that allows a user to toggle collapsible content.
JavaScript Accordion Demo
Accordions are a brilliant way of displaying content in all kinds of scenarios. This is what we’ll be building:
1. Creating the Layout (HTML)
Since we’re building a custom component, our focus will be on making it accessible so it can be operated by all users.
This is the HTML for our accordion layout:
1 |
<section id="accordion" class="accordion"> |
2 |
<div class="accordion-container"> |
3 |
<div class="accordion-item"> |
4 |
<button class="accordion-trigger" id="accordion-trigger-1" aria-expanded="false" aria-controls="accordion-content-1"> |
5 |
<span class="accordion-title"> |
6 |
</span>
|
7 |
<span class="accordion-icon"> |
8 |
+
|
9 |
</span>
|
10 |
</button>
|
11 |
<div class="accordion-content" id="accordion-content-1" role="region" aria-labelledby="accordion-trigger-1"> |
12 |
<p></p>
|
13 |
</div>
|
14 |
</div>
|
15 |
|
16 |
... |
17 |
</div>
|
18 |
</section>
|
Making Things Accessible
Let’s take a look at the attributes we use to aid accessibility:
-
aria-expanded
: this attribute is used on collapsible elements to indicate if the element is expanded or not. -
aria-controls
: this attribute is used on an element that is responsible for displaying the contents of another element. Thearia-controls
value is the id of the element being controlled. -
aria-labelledby
: this provides an accessible name to an element. -
role
: this attribute is used to assign a semantic meaning to an element. For this tutorial, we're assigning our accordion content the role region.



2. Styling the Content (CSS)
We’ll use CSS to handle the transition timing of the elements we want to animate. In this case, that’s the accordion-content
and accordion-icon
. We’ll also set the styling for when the accordion-item
is active and hide the accordion content by default.
1 |
.accordion-icon { |
2 |
transition: transform 0.5s; |
3 |
}
|
4 |
|
5 |
.accordion-item.is-active .accordion-icon { |
6 |
transform: rotate(45deg); |
7 |
}
|
8 |
|
9 |
.accordion-content { |
10 |
height: 0; |
11 |
overflow: hidden; |
12 |
transition: 0.5s; |
13 |
}
|
This is the rest of the styling for the layout:
1 |
.accordion-container { |
2 |
width: 90%; |
3 |
max-width: 1240px; |
4 |
margin: 0 auto; |
5 |
border: 3px solid #e0e0e0; |
6 |
border-radius: 24px; |
7 |
overflow: hidden; |
8 |
}
|
9 |
|
10 |
.accordion-item { |
11 |
width: 100%; |
12 |
}
|
13 |
|
14 |
.accordion-trigger { |
15 |
width: 100%; |
16 |
display: block; |
17 |
background-color: rgb(250, 250, 250); |
18 |
color: rgb(0, 0, 0); |
19 |
padding: 24px; |
20 |
font-size: 20px; |
21 |
font-weight: 500; |
22 |
font-family: 'Inter', sans-serif; |
23 |
text-align: left; |
24 |
border: none; |
25 |
display: flex; |
26 |
gap: 16px; |
27 |
justify-content: space-between; |
28 |
cursor: pointer; |
29 |
}
|
30 |
|
31 |
.accordion-item:not(:first-of-type) .accordion-trigger { |
32 |
border-top: 3px solid #eaeaea; |
33 |
}
|
34 |
|
35 |
.accordion-content { |
36 |
height: 0; |
37 |
overflow: hidden; |
38 |
transition: 0.5s; |
39 |
}
|
40 |
|
41 |
.accordion-content p { |
42 |
margin: 24px; |
43 |
}
|
One thing to note is that we don’t use any padding or margins on our accordion-content element and instead we only apply a margin to the child element. This is because height:0
doesn’t affect the margins or padding of an element so we would still have visible space if we set any.
3. Handling the Accordion Functionality (JavaScript)
First, we’ll need to get all the accordion items:
1 |
const accordionItems = document.querySelectorAll(".accordion-item"); |
Click Event Listener
Next, we’ll define a function on page load to add a click event listener to all the accordion trigger buttons.
1 |
window.addEventListener("load", () => { |
2 |
accordionItems.forEach((accordion, index) => { |
3 |
const accordionTrigger = accordion.querySelector(".accordion-trigger"); |
4 |
accordionTrigger.addEventListener("click", () => toggleAccordion(index)); |
5 |
});
|
6 |
});
|
Since our .querySelectorAll()
method returns a NodeList, we can use the .forEach()
method to loop through each accordion item element.
We can then target each trigger button inside a specific accordion item using accordion.querySelector()
. Building the component this way makes it more scalable as targeting the trigger or content element isn’t tied down to a specific id or classname and instead is based on the accordion-item container it’s present in.
Our Toggle Function
Now we’ve set up our event listener, we can define our toggleAccordion()
function.
First we’ll want to target the current accordion item. It’s possible to do this by using e.target.parentNode
but for this demo we’ll use the index instead, which the toggleFunction accepts as a parameter.
1 |
const toggleAccordion = (index) => { |
2 |
const currentAccordion = accordionItems[index]; |
3 |
currentAccordion.classList.toggle("is-active"); |
4 |
};
|
When the accordion trigger is clicked, we’ll toggle the is-active class on the corresponding accordion item.
This function will also handle updating the height of our accordion content and setting the aria-expanded value of the accordion trigger to false.
We’re setting the height in JavaScript to create a transition effect on the accordion content since there’s no way of knowing the exact height of the accordion content in CSS.
This is what our updated toggleAccordion()
function looks like:
1 |
const toggleAccordion = (index) => { |
2 |
resetAccordions(index); |
3 |
|
4 |
const currentAccordion = accordionItems[index]; |
5 |
currentAccordion.classList.toggle("is-active"); |
6 |
|
7 |
const accordionContent = currentAccordion.querySelector(".accordion-content"); |
8 |
const accordionTrigger = currentAccordion.querySelector(".accordion-trigger"); |
9 |
|
10 |
if (currentAccordion.classList.contains("is-active")) { |
11 |
accordionContent.style.height = `${accordionContent.scrollHeight}px`; |
12 |
accordionTrigger.setAttribute("aria-expanded", "true"); |
13 |
} else { |
14 |
accordionContent.style.height = 0; |
15 |
accordionTrigger.setAttribute("aria-expanded", "false"); |
16 |
}
|
17 |
};
|
Once we’ve toggled the is-active class on the currentAccordion, we’ll use the presence of the is-active class to determine the aria attributes and styling of the accordion trigger and content.
Pause for Coffee ☕️
At this point, we’ve built most of the accordion component. You deserve a coffee break!
This is what it should look like currently (try clicking on all the triggers):
Including a Reset Function
We can further expand on this build by including a reset function to make sure only one accordion is open at a time. Our reset function will loop through the accordionItems
NodeList and remove the active state on each accordion item, except for the current one based on its index.
Let's define the resetAccordions()
function:
1 |
const resetAccordions = (targetIndex) => { |
2 |
accordionItems.forEach((accordion, index) => { |
3 |
const accordionContent = accordion.querySelector(".accordion-content"); |
4 |
const accordionTrigger = accordion.querySelector(".accordion-trigger"); |
5 |
|
6 |
if (targetIndex != index) { |
7 |
accordion.classList.remove("is-active"); |
8 |
accordionContent.style.height = 0; |
9 |
accordionTrigger.setAttribute("aria-expanded", "false"); |
10 |
}
|
11 |
});
|
12 |
};
|
We loop through the accordionItem
and target all elements where the item index is not equal to the targetIndex
being passed into the resetAccordions()
function.
Finally, we can update our toggleAccordion()
function to include resetting the accordion block when one accordion item is clicked:
1 |
const toggleAccordion = (index) => { |
2 |
resetAccordions(index); |
3 |
|
4 |
const currentAccordion = accordionItems[index]; |
5 |
currentAccordion.classList.toggle("is-active"); |
6 |
|
7 |
const accordionContent = currentAccordion.querySelector(".accordion-content"); |
8 |
const accordionTrigger = currentAccordion.querySelector(".accordion-trigger"); |
9 |
|
10 |
if (currentAccordion.classList.contains("is-active")) { |
11 |
accordionContent.style.height = `${accordionContent.scrollHeight}px`; |
12 |
accordionTrigger.setAttribute("aria-expanded", "true"); |
13 |
} else { |
14 |
accordionContent.style.height = 0; |
15 |
accordionTrigger.setAttribute("aria-expanded", "false"); |
16 |
}
|
17 |
};
|
Conclusion
And with that, we’ve completely built a JavaScript accordion component! Well done. Let’s remind ourselves of the finished product:
Learn More
- Accordions on Desktop: When and How to Use Nielsen Norman Group
- Style the summary arrow with SVG alternatives (imperfect technique) Chris Coyier
- HTML Element: details