Under Construction: The content on this site draft material. It is not the official content of this site and should not be followed at this time.

Not Legal Advice: The content on this site is provided for informational purposes only and does not constitute legal advice. Consult a qualified attorney for advice specific to your situation.

Purpose

An accordion organizes related sections of content, allowing users to expand and collapse individual sections on demand. It is useful when users need to scan headings and selectively reveal detailed content, reducing page scrolling.

Usage Guidance

Use accordions when:

  • Content can be grouped into distinct, labeled sections
  • Users are likely to need only a subset of sections at once
  • Page length would be excessive if all content were visible simultaneously

Avoid accordions when:

  • Users typically need all sections open (prefer visible content)
  • Sections contain critical information that should always be visible
  • There are fewer than two or more than eight sections

Accessibility Considerations

  • Each trigger is a <button> element inside the section heading
  • aria-expanded reflects the open/closed state on the trigger button
  • aria-controls links the trigger to its content panel
  • The content panel uses hidden to hide/show content
  • No role="region" is required unless the panel contains a landmark

Keyboard Support

Key Action
Enter / Space Toggle the focused accordion panel
Tab Move focus to the next focusable element
Shift+Tab Move focus to the previous focusable element
Arrow Down Move focus to the next accordion trigger
Arrow Up Move focus to the previous accordion trigger
Home Move focus to the first accordion trigger
End Move focus to the last accordion trigger

Screen Reader Behavior

Screen readers announce the button label, its expanded/collapsed state, and the number of items. When a panel is expanded, screen reader users can navigate into the panel content naturally.

ARIA Requirements

  • aria-expanded="true|false" on each trigger <button>
  • aria-controls="panel-id" on each trigger linking to its panel
  • Each panel has a matching id attribute

Common Mistakes

  • Using <div> instead of <button> for triggers
  • Omitting aria-expanded or not updating it on state change
  • Using CSS display: none without the hidden attribute (both are fine, but be consistent)
  • Placing the trigger outside the heading element (the button should be inside <h2>, <h3>, etc.)
  • Animating height without respecting prefers-reduced-motion

Live Example


Code Example

HTML

<div class="accordion">
  <h3>
    <button
      class="accordion__trigger"
      aria-expanded="false"
      aria-controls="panel-1"
    >
      Section Title
    </button>
  </h3>
  <div id="panel-1" hidden>
    <div class="accordion__panel-inner">
      <!-- Panel content -->
    </div>
  </div>
</div>

CSS

.accordion { border: 1px solid #ccd3db; border-radius: 0.5rem; overflow: hidden; }
.accordion h3 { margin: 0; }
.accordion__trigger {
  width: 100%; text-align: left; padding: 1rem 1.25rem;
  background: #fff; border: none; border-bottom: 1px solid #ccd3db;
  font-size: 1rem; font-weight: 600; cursor: pointer;
  display: flex; align-items: center; justify-content: space-between;
}
.accordion__trigger:focus-visible {
  outline: 3px solid #d4700a;
  outline-offset: -3px;
}
.accordion__trigger::after {
  content: "";
  display: block; width: 0.6em; height: 0.6em;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg);
  transition: transform 0.2s ease;
}
.accordion__trigger[aria-expanded="true"]::after {
  transform: rotate(-135deg);
}
@media (prefers-reduced-motion: reduce) {
  .accordion__trigger::after { transition: none; }
}
.accordion__panel-inner { padding: 1rem 1.25rem; }

JavaScript

document.querySelectorAll('.accordion').forEach(function(accordion) {
  var buttons = accordion.querySelectorAll('.accordion__trigger');

  buttons.forEach(function(button) {
    button.addEventListener('click', function() {
      var expanded = button.getAttribute('aria-expanded') === 'true';
      var panel = document.getElementById(button.getAttribute('aria-controls'));
      button.setAttribute('aria-expanded', String(!expanded));
      panel.hidden = expanded;
    });

    button.addEventListener('keydown', function(e) {
      var idx = Array.from(buttons).indexOf(button);
      if (e.key === 'ArrowDown') {
        e.preventDefault();
        buttons[(idx + 1) % buttons.length].focus();
      } else if (e.key === 'ArrowUp') {
        e.preventDefault();
        buttons[(idx - 1 + buttons.length) % buttons.length].focus();
      } else if (e.key === 'Home') {
        e.preventDefault();
        buttons[0].focus();
      } else if (e.key === 'End') {
        e.preventDefault();
        buttons[buttons.length - 1].focus();
      }
    });
  });
});