Build an Accessible Accordion with HTML, CSS, and JavaScript
• Updated 6/26/2026 • HTMLCSSJavaScriptAccessibilityAccordionDOM Manipulation
Challenge Goal
Build an accessible accordion component using only HTML, CSS, and vanilla JavaScript. The accordion should let users expand and collapse content sections using a mouse, keyboard, or assistive technology.
This challenge is useful for practicing DOM manipulation, event handling, keyboard navigation, and basic web accessibility patterns.
What You Will Build
You will create a simple FAQ-style accordion with three sections. Each section will have a clickable button header and a hidden content panel.
- Only one panel should be open at a time.
- Users should be able to open panels with a mouse click.
- Users should be able to activate panels using Enter or Space.
- Users should be able to move between accordion headers using ArrowUp and ArrowDown.
- ARIA attributes should update correctly when a panel opens or closes.
Why Accessibility Matters
An accordion is not only a visual component. It must also communicate its state to screen readers and work properly for keyboard-only users.
For this challenge, you will use:
<button>elements for accordion headers.aria-expandedto show whether a panel is open or closed.aria-controlsto connect each button to its panel.role="region"to describe each panel as a content area.aria-labelledbyto connect each panel back to its button.- The
hiddenattribute to hide collapsed panels.
Requirements
- Create an accordion container with the class
accordion. - Each accordion header must be a
<button>. - Each button must control the panel directly after it.
- Only one panel may be visible at a time.
- Clicking a closed header should open its panel and close all others.
- Clicking an open header should close it.
- Pressing Enter or Space on a focused header should toggle it.
- Pressing ArrowDown should move focus to the next header.
- Pressing ArrowUp should move focus to the previous header.
- The solution must work without frameworks or external libraries.
Expected Behavior
- Initial state: all panels are closed.
- When Section 1 is opened, its panel becomes visible and
aria-expandedbecomestrue. - When Section 2 is opened, Section 1 closes automatically.
- When users reach the last header and press ArrowDown, focus moves back to the first header.
- When users reach the first header and press ArrowUp, focus moves to the last header.
Constraints
- Do not use jQuery, React, Vue, or any JavaScript framework.
- Keep the HTML, CSS, and JavaScript simple and beginner-friendly.
- Use semantic HTML whenever possible.
- Do not hard-code ARIA IDs manually for every section. Generate them with JavaScript.
- Keep the component reusable so more accordion sections can be added later.
Starter Code
Create three files: index.html, styles.css, and script.js.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accessible Accordion Challenge</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<main class="page">
<h1>Accessible Accordion</h1>
<div class="accordion">
<button class="accordion-header">What is HTML?</button>
<div class="accordion-panel">
<p>HTML is the markup language used to structure content on the web.</p>
</div>
<button class="accordion-header">What is CSS?</button>
<div class="accordion-panel">
<p>CSS is used to style web pages, including layout, colors, spacing, and typography.</p>
</div>
<button class="accordion-header">What is JavaScript?</button>
<div class="accordion-panel">
<p>JavaScript adds interactivity to web pages, such as toggles, forms, menus, and dynamic updates.</p>
</div>
</div>
</main>
<script src="script.js" defer></script>
</body>
</html>
styles.css
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Arial, sans-serif;
background: #f4f6f8;
color: #1f2937;
}
.page {
width: min(720px, 92%);
margin: 40px auto;
}
h1 {
margin-bottom: 24px;
text-align: center;
}
.accordion {
border: 1px solid #d1d5db;
border-radius: 12px;
overflow: hidden;
background: #ffffff;
}
.accordion-header {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border: 0;
border-bottom: 1px solid #e5e7eb;
background: #ffffff;
color: #111827;
font-size: 1rem;
font-weight: 700;
text-align: left;
cursor: pointer;
}
.accordion-header:hover,
.accordion-header:focus {
background: #f9fafb;
}
.accordion-header:focus-visible {
outline: 3px solid #2563eb;
outline-offset: -3px;
}
.accordion-header::after {
content: "+";
font-size: 1.25rem;
}
.accordion-header[aria-expanded="true"]::after {
content: "−";
}
.accordion-panel {
padding: 0 20px 16px;
border-bottom: 1px solid #e5e7eb;
background: #ffffff;
}
.accordion-panel p {
margin: 16px 0 0;
line-height: 1.6;
}
.accordion-panel[hidden] {
display: none;
}
script.js
// Write your JavaScript here.
Hints
- Use
document.querySelector('.accordion')to find the accordion container. - Use
accordion.querySelectorAll('.accordion-header')to collect all header buttons. - Use
nextElementSiblingto find the panel connected to each button. - Use
setAttribute()to update ARIA attributes. - Use
panel.hidden = trueandpanel.hidden = falseto hide and show panels. - Use the index of each button to generate unique IDs like
accordion-header-1andaccordion-panel-1.
Solution Walkthrough
Step 1: Select the Accordion
First, select the accordion container and all buttons inside it. If the accordion does not exist on the page, stop the script early to avoid errors.
Step 2: Add Accessibility Attributes
Each button needs an ID, an aria-controls value, and an aria-expanded value. Each panel needs an ID, role="region", and aria-labelledby.
Step 3: Close All Panels
Create a helper function that loops through every header and panel. This function should set all buttons to aria-expanded="false" and hide all panels.
Step 4: Toggle the Selected Panel
When a user clicks a header, check if it is already open. Close all panels first. Then open the selected panel only if it was previously closed.
Step 5: Add Keyboard Navigation
Add a keydown event listener to each button. Use ArrowDown and ArrowUp to move focus between buttons. Use Enter and Space to toggle the current button.
Final Solution
document.addEventListener('DOMContentLoaded', () => {
const accordion = document.querySelector('.accordion');
if (!accordion) {
return;
}
const headers = Array.from(accordion.querySelectorAll('.accordion-header'));
headers.forEach((header, index) => {
const panel = header.nextElementSibling;
if (!panel) {
return;
}
const headerId = `accordion-header-${index + 1}`;
const panelId = `accordion-panel-${index + 1}`;
header.id = headerId;
header.type = 'button';
header.setAttribute('aria-controls', panelId);
header.setAttribute('aria-expanded', 'false');
panel.id = panelId;
panel.setAttribute('role', 'region');
panel.setAttribute('aria-labelledby', headerId);
panel.hidden = true;
});
function closeAllPanels() {
headers.forEach((header) => {
const panel = header.nextElementSibling;
header.setAttribute('aria-expanded', 'false');
if (panel) {
panel.hidden = true;
}
});
}
function togglePanel(header) {
const panel = header.nextElementSibling;
const isOpen = header.getAttribute('aria-expanded') === 'true';
closeAllPanels();
if (!isOpen && panel) {
header.setAttribute('aria-expanded', 'true');
panel.hidden = false;
}
}
headers.forEach((header, index) => {
header.addEventListener('click', () => {
togglePanel(header);
});
header.addEventListener('keydown', (event) => {
const lastIndex = headers.length - 1;
if (event.key === 'ArrowDown') {
event.preventDefault();
const nextIndex = index === lastIndex ? 0 : index + 1;
headers[nextIndex].focus();
}
if (event.key === 'ArrowUp') {
event.preventDefault();
const previousIndex = index === 0 ? lastIndex : index - 1;
headers[previousIndex].focus();
}
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
togglePanel(header);
}
});
});
});
How to Test Your Accordion
- Open the page in a browser.
- Click each accordion header and confirm only one panel opens at a time.
- Click an open section again and confirm it closes.
- Use the Tab key to focus the first accordion header.
- Use ArrowDown and ArrowUp to move between headers.
- Use Enter or Space to open and close a panel.
- Inspect the HTML in DevTools and confirm
aria-expandedchanges betweentrueandfalse.
Common Mistakes
- Using clickable
<div>elements instead of real<button>elements. - Forgetting to update
aria-expandedwhen panels open or close. - Using duplicate IDs for multiple accordion sections.
- Hiding content visually but not hiding it from assistive technologies.
- Not supporting keyboard users.
Stretch Goals
- Add smooth open and close animations.
- Add a setting that allows multiple panels to stay open at the same time.
- Add icons that rotate when a panel opens.
- Create a dark mode version using CSS variables.
- Turn the accordion into a reusable function that can support multiple accordions on one page.
- Add automated tests to verify the ARIA attributes and keyboard behavior.
Conclusion
You have now built an accessible accordion using semantic HTML, CSS, and vanilla JavaScript. This challenge teaches an important front-end skill: creating interactive components that are usable by both mouse users and keyboard users.

