Skip to content

JavaScript Tutorial Practical Projects

Project 4: An Accordion

Lesson 243 of 258 2 min read By Deepak Paswan Updated Oct 4, 2026

An accordion shows a list of headings, and each one expands to reveal content. It is the right pattern for FAQs.

index.html
HTML
<style>
  .accordion { font-family: system-ui, sans-serif; max-width: 360px; }
  .accordion h3 { margin: 0; }
  .accordion button { width: 100%; display: flex; justify-content: space-between; align-items: center; font: inherit; font-weight: 600; text-align: left; padding: 10px 12px; background: #f4f9fc; border: 1px solid #c9dbe6; cursor: pointer; }
  .accordion button:focus-visible { outline: 3px solid #d94a00; outline-offset: -3px; }
  .accordion button::after { content: "+"; font-size: 20px; }
  .accordion button[aria-expanded="true"]::after { content: "−"; }
  .panel { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .25s ease; border: 1px solid #c9dbe6; border-top: 0; }
  .panel > div { overflow: hidden; }
  .panel p { margin: 0; padding: 10px 12px; }
  .panel.open { grid-template-rows: 1fr; }
  @media (prefers-reduced-motion: reduce) { .panel { transition: none; } }
</style>

<div class="accordion" id="faq">
  <h3><button aria-expanded="false" aria-controls="a1" id="q1">What is JavaScript?</button></h3>
  <div class="panel" id="a1" role="region" aria-labelledby="q1"><div><p>The programming language of the web.</p></div></div>

  <h3><button aria-expanded="false" aria-controls="a2" id="q2">Do I need HTML and CSS?</button></h3>
  <div class="panel" id="a2" role="region" aria-labelledby="q2"><div><p>They help a lot, because JavaScript works on both.</p></div></div>

  <h3><button aria-expanded="false" aria-controls="a3" id="q3">Is it free to learn?</button></h3>
  <div class="panel" id="a3" role="region" aria-labelledby="q3"><div><p>Yes. All you need is a browser.</p></div></div>
</div>

<script>
  const accordion = document.querySelector("#faq");
  const singleOpen = true;      // set to false to allow several open at once

  function setOpen(button, open) {
    button.setAttribute("aria-expanded", String(open));
    document.getElementById(button.getAttribute("aria-controls")).classList.toggle("open", open);
  }

  accordion.addEventListener("click", event => {
    const button = event.target.closest("button[aria-expanded]");
    if (!button) return;

    const willOpen = button.getAttribute("aria-expanded") !== "true";

    if (singleOpen && willOpen) {
      accordion.querySelectorAll("button[aria-expanded='true']").forEach(other => setOpen(other, false));
    }
    setOpen(button, willOpen);
  });
</script>

How it works

  • Each header is a <button> inside a heading. Enter and Space work with no extra code.
  • aria-expanded says whether it is open, and aria-controls points at the panel. The state lives in one place: setOpen() updates both the ARIA attribute and the visible state.
  • The animation uses CSS grid (grid-template-rows: 0fr to 1fr), which animates to the content’s natural height without measuring it in JavaScript. It is turned off for people who prefer reduced motion.
  • Event delegation: one listener on the whole accordion.

Closed panels are hidden from assistive technology?

The panels animate to zero height with overflow: hidden, but their text is still in the page. For stricter behaviour, also set hidden or inert on closed panels, or use the native <details> element. It handles most of this with no JavaScript:

index.html
HTML
<details>
  <summary>What is JavaScript?</summary>
  <p>The programming language of the web.</p>
</details>

Try changing it

  1. Set singleOpen = false to allow many panels at once.
  2. Add Expand all and Collapse all buttons.
  3. Open the panel whose id is in the URL hash.
  4. Replace the whole thing with <details name="faq"> (a shared name makes only one open at a time in current browsers).
info
FAQ markup
If this accordion is a real FAQ page, plain text content that is always in the HTML is good for search engines and accessibility.