Skip to main content

Accordion

An accordion is a vertically stacked set of sections whose headers reveal or hide related content.

User activates a header
↓
Update the open section state
↓
Expose or hide its panel
↓
Update the header's expanded state

Accordions are commonly used for FAQs, settings, course modules, filters, and compact supporting information.

Core idea: An accordion connects each interactive header to one content panel and keeps their visible and accessible states synchronized.


1. Accordion Anatomy​

Every accordion item has two important parts.

Accordion
├── Header button: “Shipping information”
│ └── expanded or collapsed state
└── Panel
└── content controlled by that button
PartResponsibility
HeaderNames the section and toggles it
PanelContains the content associated with the header
IndicatorVisually communicates open or closed state
Accordion groupCoordinates single-open or multiple-open behavior

The whole header should usually be clickable, not only a small chevron icon.


2. When to Use an Accordion​

Use an accordion when:

  • users need only a few sections at a time;
  • headings make hidden content predictable;
  • showing everything would create unnecessary scrolling or clutter;
  • the content remains understandable when sections are independently collapsed.

Avoid or reconsider it when:

  • users must compare content across sections;
  • most users need every section;
  • essential information may be overlooked when collapsed;
  • there are only one or two short pieces of content;
  • the interaction is actually navigation rather than disclosure.

Accordions reduce visible content, but they also hide information and add interaction cost. Compactness alone does not make them the right pattern.


3. Accordion vs Similar Patterns​

PatternPurpose
AccordionShow or hide vertically stacked content sections
DisclosureShow or hide one independent piece of content
TabsSwitch between peer views inside one content area
Dropdown menuPresent actions or navigation choices
Tree viewNavigate hierarchical items with nested branches

Do not give an accordion tab roles. Tabs and accordions have different semantics and keyboard expectations.


4. Start with Native HTML​

For a simple disclosure, <details> and <summary> provide open/close behavior and keyboard interaction without React state.

<details>
<summary>What is the return policy?</summary>
<p>Items can be returned within 30 days of delivery.</p>
</details>

Multiple native disclosures can remain open independently:

<details>
<summary>Shipping</summary>
<p>Standard delivery takes three to five working days.</p>
</details>

<details>
<summary>Returns</summary>
<p>Returns are accepted within 30 days.</p>
</details>

Modern browsers also support exclusive groups through the same name:

<details name="product-help">
<summary>Shipping</summary>
<p>Standard delivery takes three to five working days.</p>
</details>

<details name="product-help">
<summary>Returns</summary>
<p>Returns are accepted within 30 days.</p>
</details>

Only one grouped <details> element can be open at a time.

Use native HTML when it satisfies the design. Build a custom component when the application needs controlled state, design-system integration, analytics, coordinated behavior, or more specialized content lifecycle rules.


5. Choose the State Model​

There are two common accordion behaviors.

Single-Open Accordion​

Only one panel can be open.

const [openId, setOpenId] = useState(null);

function toggle(id) {
setOpenId(current => (current === id ? null : id));
}
openId = null       → every panel is closed
openId = "shipping" → only Shipping is open

Multiple-Open Accordion​

Several panels can be open together.

const [openIds, setOpenIds] = useState(() => new Set());

function toggle(id) {
setOpenIds(current => {
const next = new Set(current);

if (next.has(id)) next.delete(id);
else next.add(id);

return next;
});
}

Do not mutate the existing Set directly. React state should receive a new collection so the update is observable and predictable.


6. Use Stable IDs, Not Array Positions​

Accordion data should include stable identifiers.

const faqItems = [
{
id: "shipping",
title: "How long does shipping take?",
content: "Standard delivery takes three to five working days.",
},
{
id: "returns",
title: "Can I return an item?",
content: "Yes, within 30 days of delivery.",
},
];

Using an array index for both state and React's key can attach open state to the wrong item after insertion, deletion, filtering, or reordering.

// Prefer a stable ID from the data.
<AccordionItem key={item.id} id={item.id} />

7. Accessible React Implementation​

This version supports either single-open or multiple-open behavior.

import { useState } from "react";

export default function Accordion({
items,
allowMultiple = false,
defaultOpenIds = [],
}) {
const [openIds, setOpenIds] = useState(
() => new Set(defaultOpenIds)
);

function toggleItem(id) {
setOpenIds(current => {
const isOpen = current.has(id);
const next = allowMultiple ? new Set(current) : new Set();

if (isOpen) next.delete(id);
else next.add(id);

return next;
});
}

return (
<div className="accordion">
{items.map(item => (
<AccordionItem
key={item.id}
item={item}
isOpen={openIds.has(item.id)}
onToggle={() => toggleItem(item.id)}
/>
))}
</div>
);
}

function AccordionItem({ item, isOpen, onToggle }) {
const headerId = `accordion-header-${item.id}`;
const panelId = `accordion-panel-${item.id}`;

return (
<section className="accordion__item">
<h3 className="accordion__heading">
<button
id={headerId}
type="button"
aria-expanded={isOpen}
aria-controls={panelId}
className="accordion__trigger"
onClick={onToggle}
>
<span>{item.title}</span>
<span aria-hidden="true">{isOpen ? "−" : "+"}</span>
</button>
</h3>

<div
id={panelId}
role="region"
aria-labelledby={headerId}
hidden={!isOpen}
className="accordion__panel"
>
<p>{item.content}</p>
</div>
</section>
);
}

Why These Attributes Matter​

AttributePurpose
type="button"Prevents an accordion inside a form from submitting it
aria-expandedAnnounces the current open or closed state
aria-controlsIdentifies the panel controlled by the button
aria-labelledbyGives the panel the header's accessible name
hiddenRemoves collapsed content from display and interaction

Use role="region" selectively. Adding it to many simultaneously expandable panels can create too many page landmarks.


8. Keyboard Interaction​

A native <button> already supports the essential behavior.

KeyBehavior
TabMoves focus to the next focusable control
Shift + TabMoves focus to the previous focusable control
EnterToggles the focused accordion button
SpaceToggles the focused accordion button

Do not replace the button with a clickable <div>. A <div> does not automatically provide button semantics, keyboard activation, focus behavior, or disabled behavior.

Some design systems add arrow-key navigation between headers. If added, it must be implemented and tested consistently; it is not necessary for a basic accordion whose headers are ordinary buttons in the tab order.


9. Controlled vs Uncontrolled Accordion​

Uncontrolled​

The accordion owns its open state.

<Accordion items={faqItems} defaultOpenIds={["shipping"]} />

This is suitable when no parent feature needs to control the selected section.

Controlled​

The parent owns the state and passes it down.

<Accordion
items={faqItems}
openIds={openIds}
onOpenChange={setOpenIds}
/>

Controlled state is useful when:

  • the URL opens a specific section;
  • validation opens a section containing an error;
  • another control expands or collapses sections;
  • analytics or application rules depend on changes.

Choose one ownership model and document it clearly. Switching between controlled and uncontrolled behavior during a component's lifetime creates confusing state.


10. Preserve or Reset Panel Content​

There are two ways to hide a panel.

Keep It Mounted​

<div hidden={!isOpen}>{children}</div>

Local form input, media state, and child component state are preserved while the panel is closed.

Unmount It​

{isOpen && <div>{children}</div>}

The component is removed and its local state resets. This may save work for expensive panels, but reopening can repeat initialization or data fetching.

This is a product and lifecycle decision, not only a rendering preference.


11. Animation​

Animating from height: 0 to height: auto is not a reliable simple CSS transition. Common approaches include:

  • animating a measured pixel height;
  • using a grid-row expansion technique;
  • using the Web Animations API;
  • using an established motion component from the design system.

Regardless of the technique:

  • keep duration short;
  • avoid animating expensive layout repeatedly;
  • prevent collapsed controls from receiving focus;
  • respect prefers-reduced-motion;
  • keep visual state synchronized with aria-expanded.
@media (prefers-reduced-motion: reduce) {
.accordion__panel {
transition: none;
}
}

An animation should clarify the relationship between the header and panel, not delay access to content.


12. Deep Linking and URL State​

For documentation or settings pages, a section may need a shareable URL.

/help#returns
/settings?section=privacy

On navigation:

  1. read and validate the requested section ID;
  2. open that section;
  3. move or scroll to it only when appropriate;
  4. preserve expected Back and Forward behavior.

Do not store all accordion interactions in the URL automatically. Use URL state when a section is meaningful enough to bookmark or share.


13. Performance Considerations​

Most accordions do not need special optimization. For complex panels:

  • defer expensive work until the first expansion;
  • cache fetched content when reopening should be fast;
  • cancel obsolete requests if a panel unmounts;
  • avoid loading data on every toggle;
  • keep state close to the component that owns it;
  • profile before adding memoization.

If panels contain forms, decide whether closing a panel should preserve unsaved work and communicate that behavior to users.


14. Common Mistakes​

MistakeBetter approach
Making only the chevron clickableMake the complete header a button
Using a clickable <div>Use a native <button>
Updating only the visual iconKeep visibility and aria-expanded synchronized
Using array indexes as identityUse stable IDs from the data
Creating separate state for every panelStore one open ID or a collection of open IDs
Hiding content visually but leaving it focusableUse hidden or otherwise remove collapsed descendants from interaction
Using tab roles for an accordionFollow disclosure/accordion semantics
Animating while ignoring reduced-motion preferencesProvide a reduced or instant transition
Collapsing a form and unexpectedly deleting its dataChoose mounting behavior intentionally
Nesting many interactive controls inside the header buttonKeep the button's content simple and valid

15. Testing Checklist​

Behavior​

  • Clicking a closed header opens its panel.
  • Clicking an open header closes it when collapsing is allowed.
  • Single-open mode closes the previously open panel.
  • Multiple-open mode preserves other open panels.
  • Reordered data keeps state attached to the correct IDs.

Keyboard and Accessibility​

  • Every header is reachable with Tab.
  • Enter and Space toggle the focused header.
  • Focus remains on the activated header.
  • aria-expanded matches visible state.
  • aria-controls and aria-labelledby point to existing unique IDs.
  • Collapsed content cannot receive focus.
  • Screen readers announce meaningful header names and state.

Layout​

  • Long and translated headings wrap correctly.
  • Icons remain understandable at zoomed text sizes.
  • Animation works without clipping dynamic content.
  • Reduced-motion settings are respected.

Official References​


Summary​

An accordion is state-driven disclosure: each accessible header controls one panel, while the group decides how many panels may remain open.


Think of every accordion item as a synchronized pair:

Header button
├── meaningful label
├── aria-expanded
└── aria-controls ───────────┐
↓
Content panel
├── matching ID
├── labelled by the header
└── visible only when expanded

The React state determines which pair is open. Stable identity, native controls, and matching accessibility attributes make that state understandable to every user.