JavaScript Tutorial
JavaScript Project: Accordion
FAQ panels that open one at a time. Click a question to show its answer and hide the others.
What you will build
A short FAQ. Each item is a question button and an answer panel. Click a question to open its answer. Opening one item closes the others, so only one panel is visible.
Clicking the already-open question closes it. That gives you a way back to an all-closed list without a separate collapse-all control.
Open an example with Try it in JavaScript. That loads /javascript/try — a live page and a console.
Methods you will use
| Method | Job on this page |
|---|---|
classList | Adds or removes open on a panel |
querySelectorAll | Finds every FAQ item so others can close |
click | Toggles the item that was pressed |
contains | Checks whether the clicked item was already open |
aria-expanded | Tells assistive tech which question is expanded |
CSS hides answers unless the parent has class open. JavaScript does not setdisplay on each panel. It only flips a class. Layout stays in the stylesheet.
Build in slices
Three items, same structure. The question is a button so it is keyboard reachable. The answer is a div under it, not a sibling elsewhere in the page.
Example
<div class="item">
<button type="button" class="question" aria-expanded="false">What is an accordion?</button>
<div class="answer">A stack of panels where opening one closes the rest.</div>
</div>
<div class="item">
<button type="button" class="question" aria-expanded="false">Why not use details?</button>
<div class="answer">details can open several at once. This script keeps one open.</div>
</div>Hide answers with CSS. The open class on the item reveals the panel. Start with everything closed.
Example
<style>
.answer { display: none; padding: 0.5rem 0.75rem 0.85rem; }
.item.open .answer { display: block; }
.question { width: 100%; text-align: left; }
</style>
<div class="item open">
<button type="button" class="question">What is an accordion?</button>
<div class="answer">This panel is open because the item has class open.</div>
</div>On click, remember whether this item was open, close every item, then reopen this one only if it was closed. Update aria-expanded to match.
Example
<style>
.answer { display: none; }
.item.open .answer { display: block; }
.question { width: 100%; text-align: left; }
</style>
<div class="item">
<button type="button" class="question" aria-expanded="false">First question</button>
<div class="answer">First answer.</div>
</div>
<div class="item">
<button type="button" class="question" aria-expanded="false">Second question</button>
<div class="answer">Second answer. Opening this closes the first.</div>
</div>
<script>
const items = document.querySelectorAll(".item");
items.forEach(function (item) {
const button = item.querySelector(".question");
button.addEventListener("click", function () {
const wasOpen = item.classList.contains("open");
items.forEach(function (other) {
other.classList.remove("open");
other.querySelector(".question").setAttribute("aria-expanded", "false");
});
if (!wasOpen) {
item.classList.add("open");
button.setAttribute("aria-expanded", "true");
}
});
});
</script>Open the first panel, then the second, at /javascript/try. The first answer must hide. Use Try it in JavaScript.
Complete document
This file is the FAQ you would keep. Four questions about the JavaScript studio. The open panel gets a darker amber header so you can see which item is active.
Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Accordion</title>
<style>
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
background: #fefce8;
font-family: "Segoe UI", system-ui, sans-serif;
color: #1c1917;
}
.card {
width: min(28rem, 92vw);
background: #fffbeb;
border: 1px solid #fde68a;
padding: 1.3rem 1.3rem 1.4rem;
}
h1 { margin: 0 0 0.85rem; font-size: 1.4rem; }
.item { border: 1px solid #fde68a; margin-bottom: 0.45rem; background: #fff; }
.question {
width: 100%;
text-align: left;
padding: 0.65rem 0.75rem;
border: 0;
background: #fef9c3;
font: inherit;
font-weight: 700;
cursor: pointer;
}
.item.open .question { background: #854d0e; color: #fffbeb; }
.answer { display: none; padding: 0.65rem 0.75rem 0.85rem; line-height: 1.5; }
.item.open .answer { display: block; }
</style>
</head>
<body>
<article class="card">
<h1>JavaScript FAQ</h1>
<div class="item">
<button type="button" class="question" aria-expanded="false">Where does Try it open?</button>
<div class="answer">Try it in JavaScript opens /javascript/try. It is not /try and not /html/try.</div>
</div>
<div class="item">
<button type="button" class="question" aria-expanded="false">Why keep one panel open?</button>
<div class="answer">A single open panel keeps the FAQ short on small screens. Two answers at once bury the next question.</div>
</div>
<div class="item">
<button type="button" class="question" aria-expanded="false">What does classList do?</button>
<div class="answer">classList.add and classList.remove change the open class. CSS decides whether the answer is visible.</div>
</div>
<div class="item">
<button type="button" class="question" aria-expanded="false">Can I click the open question again?</button>
<div class="answer">Yes. The second click closes it. Every panel can be shut without a separate button.</div>
</div>
</article>
<script>
const items = document.querySelectorAll(".item");
function closeAll() {
items.forEach(function (item) {
item.classList.remove("open");
item.querySelector(".question").setAttribute("aria-expanded", "false");
});
}
items.forEach(function (item) {
const button = item.querySelector(".question");
button.addEventListener("click", function () {
const wasOpen = item.classList.contains("open");
closeAll();
if (!wasOpen) {
item.classList.add("open");
button.setAttribute("aria-expanded", "true");
}
});
});
</script>
</body>
</html>One open panel
Close all first, then open the clicked item. If you open first and close the others second, a loop that includes the current item would immediately close what you just opened.
Native details / summary can do an FAQ without a script, but several can stay open. This project is specifically exclusive open. That is why the script walks every item on each click.
Common mistakes
- Toggling only the clicked item. Two panels stay open. That is a disclosure list, not this accordion.
- Putting the listener on
.answer. Clicks on the question would do nothing. - Using
displayin JavaScript. Then CSS and script fight. Flip a class. - Selecting
document.querySelector(".item")(one node) instead ofquerySelectorAll. - Forgetting
type="button"if the FAQ later sits in a form. The page would submit.
Practice tasks
- Open the first panel on load by adding
openandaria-expanded="true"after binding clicks. - Add a fifth question. You should not need to change the script.
- Allow several panels open only when Shift is held during the click. Preview at /javascript/try.