This article covers two ways to build an accordion menu: the modern HTML-only approach, and the CSS plus jQuery approach.

The short answer: if you are building one today, <details> and <summary> do the whole job with no JavaScript. Keyboard support and screen reader semantics come built in.

If jQuery is already on the page, or you need fine control over the open and close behaviour, the jQuery version below still applies. Both are covered so you can pick the one that fits.

The accordion we are building

See the Pen Accordion menu by rin (@rinblog0408)
on CodePen.

Building it with details and summary

<details> and <summary> give you an accordion with no CSS and no JavaScript at all, and it is keyboard accessible from the start.

<details name="faq">
  <summary>Section 1 heading</summary>
  <p>The content of section 1 goes here.</p>
</details>

<details name="faq">
  <summary>Section 2 heading</summary>
  <p>The content of section 2 goes here.</p>
</details>

The name attribute is the important part. Elements sharing a name become exclusive: opening one closes the others. That is the same behaviour the jQuery version below has to implement by hand, delivered by a single attribute.

Styling it takes very little:

details {
  border: 1px solid #ddd;
  margin-bottom: 10px;
}

summary {
  background-color: #f1f1f1;
  padding: 10px;
  cursor: pointer;
}

details > *:not(summary) {
  padding: 0 10px 10px;
}

Animating the open and close

Transition the height of the ::details-content pseudo-element to make it slide.

/* allow animating to height: auto */
:root {
  interpolate-size: allow-keywords;
}

details::details-content {
  height: 0;
  overflow: hidden;
  transition: height 0.3s, content-visibility 0.3s;
  transition-behavior: allow-discrete;
}

details[open]::details-content {
  height: auto;
}

interpolate-size: allow-keywords is what makes a transition from height: 0 to height: auto possible. Without it the browser has no target height and the panel snaps open.

transition-behavior: allow-discrete keeps the content rendered while it closes. Without it the content vanishes the instant the closing animation starts.

The animation is Chromium-only as of September 2026. Elsewhere the accordion still opens and closes correctly, just without the slide, so this is safe to ship as a progressive enhancement.

Building the basic structure by hand

The HTML

The markup below is the starting point for the CSS and jQuery version: three sections, each with a heading and a content panel.

<div class="accordion">
  <div class="accordion-section">
    <div class="accordion-header">Section 1 heading</div>
    <div class="accordion-content">
      The content of section 1 goes here.
    </div>
  </div>
  <div class="accordion-section">
    <div class="accordion-header">Section 2 heading</div>
    <div class="accordion-content">
      The content of section 2 goes here.
    </div>
  </div>
</div>

The CSS

.accordion-section {
  border: 1px solid #ddd;
  margin-bottom: 10px;
}

.accordion-header {
  background-color: #f1f1f1;
  padding: 10px;
  cursor: pointer;
}

.accordion-content {
  display: none;
  padding: 10px;
}

.active {
  display: block;
}

Adding the behaviour with jQuery

<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script>
  $(document).ready(function() {
    $('.accordion-header').on('click', function() {
      const $header = $(this);
      const $content = $header.next('.accordion-content');
      const isOpen = $header.hasClass('active');

      // close everything first
      $('.accordion-content').slideUp();
      $('.accordion-header').removeClass('active').attr('aria-expanded', 'false');

      // reopen only if it was closed
      if (!isOpen) {
        $header.addClass('active').attr('aria-expanded', 'true');
        $content.slideDown();
      }
    });
  });
</script>

When clicking an open panel fails to close it

Calling “close everything” and then slideToggle() makes an open panel close and immediately reopen.

slideUp() is animated, so the panel is not yet closed on the next line. slideToggle() then reads it as still open and opens it again.

The code above records the state in isOpen before closing anything, then reopens only if the panel was closed. Clicking an open panel now reliably closes it.

Making it keyboard accessible

A <div> cannot be reached with the keyboard. Use a <button> for the heading.

<div class="accordion-header"> responds to clicks, but it cannot be focused with Tab and does not respond to Enter. For anyone not using a mouse, the accordion simply does not work.

<div class="accordion-section">
  <button type="button" class="accordion-header" aria-expanded="false" aria-controls="panel1">
    Section 1 heading
  </button>
  <div class="accordion-content" id="panel1">
    The content of section 1 goes here.
  </div>
</div>

A button brings default styling, so reset it:

.accordion-header {
  display: block;
  width: 100%;
  text-align: left;
  border: none;
  font: inherit;
  background-color: #f1f1f1;
  padding: 10px;
  cursor: pointer;
}

aria-expanded tells screen readers whether the panel is open. The JavaScript above keeps it in sync.

Which approach to choose

Use <details> for new work and the jQuery version when you are adding to an existing site.

▼The two approaches compared

details / summary CSS + jQuery
JavaScript None Required
Keyboard support Built in Requires a button element
Screen readers Built in Requires ARIA attributes
Only one open at a time The name attribute Written in JavaScript
Open/close animation Chromium only Everywhere
Fine-grained control Limited Complete

Unless the animation is a hard requirement, choose <details>. You write less, and there is no accessibility work to forget. Add JavaScript when a real requirement appears, such as running something else when a panel opens.

For the same show-and-hide logic applied to navigation, see 5 ways to build a jQuery dropdown menu, which also covers the CSS-only and <details> alternatives.

If you get stuck walking from a clicked element to the panel it controls, getting parent elements in jQuery explains when to use .closest() instead of .parents().

ABOUT ME
りん
On this blog, I mainly share information about web development and programming, along with my daily thoughts and what I’ve learned. I aim to create a blog that lets readers enjoy both technology and everyday life, so I also include topics about daily experiences, books, and gourmet. I’d be delighted if you could drop by casually and find something useful or enjoyable here.