Show/Hide & Expand
Accordion
AvailableA UI where pressing a heading toggles a panel open or closed. Uses a button with aria-expanded to convey the current state to assistive technologies.
Specification updated
- Breaking#3434(opens in a new tab)Accordion: Remove arrow key and Home/End navigation from spec
What Is an Accordion?
An accordion is a UI pattern where pressing a heading (header) expands or collapses the panel beneath it. It is commonly used in FAQs, settings screens, product details, and other situations where content should be hidden by default and revealed only when needed.
Although it looks simple, it is essential to convey whether a section is currently expanded or collapsed to users who cannot see the screen. This is the key accessibility concern.
Why Does Accessibility Matter?
It helps to imagine two types of users.
- People who rely on the keyboard only (those who cannot or find it difficult to use a mouse). If the heading is a
<div>, it cannot receive focus via Tab, making it impossible to open. - People who use a screen reader (those who listen to the screen rather than see it). A plain
<div>is not recognized as a "button," so users cannot tell that it is interactive or whether the section is currently open.
The shortest path to satisfying both requirements is to use a <button> for the heading and communicate the state with aria-expanded.
Live Demo (Recommended Implementation)
The accordion below is implemented according to the APG. Try operating it using only the keyboard, without a mouse.
Try it: Tab to a button, then press Enter or Space to expand/collapse. Move between headings with Tab.
Tip
Turn on a screen reader (on macOS, press Cmd+F5 for VoiceOver) and focus a button. You will hear something like "collapsed, About Shipping, expanded" -- therole and state are announced. This is the effect of aria-expanded.
Keyboard Interaction
| Key | Action | Priority |
|---|---|---|
| Enter / Space | Toggle the focused heading panel open/closed | Required |
| Tab | Move to the next focusable element (e.g. next heading) | Required |
Note
Expanding and collapsing with Enter and Space comes automaticallywhen the heading is a <button> -- you do not need to write any key-handling code yourself. This is the power of native elements.
Required WAI-ARIA Roles and Properties
| Target | Attribute / Role | Meaning |
|---|---|---|
| Heading button | aria-expanded="true | false" | Whether the corresponding panel is open. Must be updated whenever the state changes. |
| Heading button | aria-controls="panelId" | Indicates which panel this button controls. |
| Wrapper around heading | <h2> to <h4> | Wrap with a heading level appropriate to the page structure. Helps screen reader heading navigation. |
| Panel | role="region" + aria-labelledby="headingId" | Makes the panel a named region (optional but recommended; omit when there are too many regions as it becomes counterproductive). |
| Collapsed panel | hidden | Hide from the DOM while collapsed, removing it from keyboard navigation and screen reader access. |
Implementation: Recommended Pattern (Good)
Good / Recommended
Use a <button> for the heading and keep aria-expanded in sync with the panel state.
Markup:
<div class="accordion">
<h3>
<button type="button"
id="acc-h-1"
aria-expanded="true"
aria-controls="acc-p-1">
About Shipping
</button>
</h3>
<div id="acc-p-1"
role="region"
aria-labelledby="acc-h-1">
Your order will be delivered within 2-4 business days.
</div>
<h3>
<button type="button"
id="acc-h-2"
aria-expanded="false"
aria-controls="acc-p-2">
Can I return or exchange items?
</button>
</h3>
<div id="acc-p-2"
role="region"
aria-labelledby="acc-h-2"
hidden>
Returns are accepted within 14 days of delivery.
</div>
</div>Toggle script (state synchronization is all you need):
const triggers = document.querySelectorAll('.accordion button');
triggers.forEach((trigger) => {
trigger.addEventListener('click', () => {
const expanded = trigger.getAttribute('aria-expanded') === 'true';
const panel = document.getElementById(
trigger.getAttribute('aria-controls')
);
// Toggle the state and keep aria-expanded in sync with visibility
trigger.setAttribute('aria-expanded', String(!expanded));
panel.hidden = expanded;
});
});Note
If you want an accordion where only one section can be open at a time, setaria-expanded to false on the other buttons and add hidden to their panels before opening the new one. However, it is courteous to allow all sections to be closed (this is also recommended by the APG).
Anti-Pattern (Bad)
The example below is an accordion built with only <div> and onclick -- it looks the same but only works visually.It works with a mouse, but it is completely inoperable with a keyboard.Try pressing Tab then Enter and compare the experience with the demo above.
Try it: Tab does not focus the heading. A screen reader does not recognize it as a button and cannot convey that it is expandable.
<!-- anti-pattern -->
<div class="accordion">
<!-- div cannot receive keyboard focus -->
<div class="trigger" onclick="toggle('p1')">
About Shipping +
</div>
<div id="p1" style="display:none">
Your order will be delivered within 2-4 business days.
</div>
</div>Bad / Avoid
Problems with this implementation:
- Not keyboard accessible -- a
divcannot receive focus. - Role is not communicated -- a screen reader treats it as plain text and does not indicate it is interactive.
- State is not communicated -- without
aria-expanded, users cannot tell whether the section is open or closed. - Uses
display:noneinstead of thehiddenattribute, relying on inline styles with no proper state management.
Tip
If you must use a <div>, you need to add role="button", tabindex="0",Enter/Space key handling, and aria-expanded -- all manually. Using a <button> from the start gives you most of this for free.
Implementation Checklist
- The heading trigger is a <button type="button">
- The button is wrapped in a heading element (<h2> through <h4>)
- aria-expanded always matches the open/closed state
- aria-controls points to the panel id
- Collapsed panels are hidden from keyboard and screen readers with the hidden attribute
- Panel has role="region" + aria-labelledby (optional but recommended)
- Can be toggled with keyboard alone and focus is visible (focus ring is not removed)
Source (English):Accordion Pattern — W3C APG(opens in a new tab)