Build a React accordion by choosing whether panels open independently or one at a time, storing that state in React, and connecting each native button trigger to its panel. Give every trigger an accurate aria-expanded value and an aria-controls reference to the panel it operates. The W3C Accordion Pattern defines the expected semantics and keyboard behavior; React’s state-sharing guidance shows how to coordinate items through a common parent.
Choose how the accordion should open
An accordion is a vertical set of interactive headings whose controls reveal or hide associated panels. Decide its open-state policy before writing the component: it determines what state to store and what users can do.
As an Amazon Associate I earn from qualifying purchases.
| Behavior | State to store | Can every panel be closed? | When it fits |
|---|---|---|---|
| Independent panels | A collection of open item IDs | Yes, unless you deliberately enforce another rule. | Readers may need to compare or keep several answers visible. |
| One panel at a time | One active item ID, or null when none is open |
Yes if toggling the active item sets the ID to null; otherwise one item can remain required open. |
The interface should show a single section at once. |
For a controlled accordion, keep the state in a parent that needs to coordinate or control it, then pass each item its expanded value and a toggle callback. For a self-contained accordion, keep that state inside the accordion. React describes this choice as a trade-off between flexibility and configuration; see Sharing State Between Components.
Recommended Free Tools
Build a single-open accordion
This example lets users close the open panel, so the initial state is null and clicking the active item returns it to null. Each item receives its expanded state from the parent; the same value drives both the button’s ARIA state and the panel’s visibility.
#1 Best Overall
import { useState } from 'react';
function AccordionItem({ id, title, expanded, onToggle, children }) {
const buttonId = `${id}-trigger`;
const panelId = `${id}-panel`;
return (
<section>
<h3>
<button
id={buttonId}
type="button"
aria-expanded={expanded}
aria-controls={panelId}
onClick={onToggle}
>
{title}
</button>
</h3>
<div id={panelId} hidden={!expanded}>
{children}
</div>
</section>
);
}
function Accordion({ items }) {
const [activeId, setActiveId] = useState(null);
return (
<div>
{items.map((item) => {
const expanded = activeId === item.id;
return (
<AccordionItem
key={item.id}
{...item}
expanded={expanded}
onToggle={() =>
setActiveId(expanded ? null : item.id)
}
/>
);
})}
</div>
);
}
Pass an items array with a unique, stable id and a title for each entry; the item’s remaining content becomes its children. The heading level in this example is h3: choose the level that fits the page outline, rather than copying it regardless of context. Keep item IDs unique so each generated trigger and panel ID—and each aria-controls relationship—is unique.
Allow multiple panels to stay open
For independent panels, store a collection of open IDs in the parent instead of a single active ID. Toggling an item adds or removes only that item’s ID; the item component and its button-panel markup can stay the same.
function MultiOpenAccordion({ items }) {
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;
});
}
return (
<div>
{items.map((item) => {
const expanded = openIds.has(item.id);
return (
<AccordionItem
key={item.id}
{...item}
expanded={expanded}
onToggle={() => toggle(item.id)}
/>
);
})}
</div>
);
}
This uses the functional form of the state setter so each update works from the latest state. If your design must always leave one panel open, initialize with that panel’s ID and prevent toggling the active item closed. The W3C pattern says that when an expanded panel cannot be collapsed, its trigger should use aria-disabled="true" to indicate that restriction.
Use accessible trigger and panel semantics
The W3C pattern specifies a native button inside a heading for each accordion trigger. In the examples, aria-expanded receives the same boolean that controls the panel’s hidden attribute, so the announced state and visible state stay aligned. aria-controls points to the associated panel’s ID.
Rank #3
- Keep the button’s
type="button"so it does not accidentally submit a surrounding form. - Use Enter or Space to activate the focused button. Native buttons provide these behaviors without recreating keyboard interaction on a clickable
div. - Tab and Shift+Tab move through focusable elements in normal page order. Keep links and other focusable content inside panels in that order.
- Provide a visible focus style in the finished design, and verify that the trigger remains identifiable while keyboard focus is on it.
The panel’s region role is optional. Add role="region" and aria-labelledby only when making the panel a landmark is useful; applying region landmarks to every item in a large accordion can create landmark clutter. The W3C pattern specifically cautions against region roles when more than approximately six panels can be expanded simultaneously. The basic example therefore leaves the role off.
The pattern’s listed keyboard interaction is Enter, Space, Tab, and Shift+Tab. Arrow-key, Home, and End navigation are optional enhancements, not required behavior in that pattern; add them only if the component implements and documents them consistently.
Rank #4
Test the behavior before shipping
- Activate every trigger with both a pointer and the keyboard; confirm it opens and, where allowed, closes its panel.
- Check that only the intended panel changes in independent mode, or that opening a second item closes the first in single-open mode.
- Inspect each rendered button: its
aria-expandedvalue should match whether its associated panel is visible, and itsaria-controlsvalue should match that panel’s ID exactly. - Confirm focus is visible and that Tab and Shift+Tab follow the expected page order through buttons and any focusable panel content.
These checks describe expected implementation behavior, not a claim of browser or assistive-technology testing. Test the finished component in the application and environments you support.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallQuick Recap
Best Value
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




