LEARNING OBJECTIVES ⌵
- Understand the structural anatomy of the W3C APG Accordion pattern.
- Correctly nest native
<button>triggers inside semantic heading tags (<h3>). - Synchronize dynamic disclosure states using
aria-expanded="true|false"andaria-controls. - Compare native HTML5
<details>/<summary>with custom WAI-ARIA Accordion implementations.
📖 The Mental Model & Story (Intuitive Foundation)
Imagine an accordion musical instrument. When compressed, its pleated bellows take up very little physical space. When the musician pulls the handles apart, the bellows expand, allowing air to flow through the internal reeds and create rich sound.
In user interface design, an Accordion operates on the exact same principle of spatial efficiency. When you have a massive FAQ page containing 25 extensive answers, presenting all 25 answers expanded simultaneously creates a 10,000-pixel vertical wall of text.
By grouping related content into collapsible panels, users can quickly scan the compressed headers to find what they need, and selectively expand only the specific answer they wish to read.
Without ARIA, custom accordions built with <div> click handlers leave screen reader users completely unaware of whether a section is open, closed, or what content appeared when clicked.
The WAI-ARIA Accordion Pattern provides clear audio feedback: "Frequently Asked Questions. Heading level 3, button: How do I reset my password? collapsed. Press Space to expand."
Technical Deep Dive & Specifications
The Accordion Structural Anatomy
+-----------------------------------------------------------------------------+
| ACCORDION SECTION ANATOMY |
+-----------------------------------------------------------------------------+
| |
| <h3> |
| <button |
| type="button" |
| id="acc-header-1" |
| aria-expanded="false" |
| aria-controls="acc-panel-1"> |
| What is your refund policy? |
| </button> |
| </h3> |
| |
| <div |
| role="region" |
| id="acc-panel-1" |
| aria-labelledby="acc-header-1" |
| hidden> |
| <p>We offer a 30-day money-back guarantee with zero hassle...</p> |
| </div> |
| |
+-----------------------------------------------------------------------------+
The 3 Core Requirements of WAI-ARIA Accordions
Heading Containment (
<h3><button>...</button></h3>):- The accordion trigger must be a native
<button>nested inside a semantic heading (<h2>through<h6>). - This ensures screen reader users can navigate the page using both heading shortcuts (
Hkey) and form/button shortcuts.
- The accordion trigger must be a native
State & Association Attributes:
aria-expanded="true|false"on the<button>: Conveys whether the controlled panel is visible.aria-controls="panel-id"on the<button>: Identifies which panel container is toggled.aria-labelledby="header-btn-id"on the panel: Links the panel back to its header.role="region"on the panel: Exposes the expanded panel as a distinct landmark region once open.
Panel Visibility Management:
- Inactive panels must be hidden using the HTML5
hiddenattribute or CSSdisplay: none. - Collapsed content must never be accessible in the keyboard tab order while hidden.
- Inactive panels must be hidden using the HTML5
Keyboard Interaction Contract
| Keystroke | Behavior |
|---|---|
Enter or Space |
Toggles the expanded/collapsed state of the focused accordion header button. |
Tab |
Moves focus to the next focusable element (either the next accordion header or an interactive link/button inside an open panel). |
Shift + Tab |
Moves focus to the previous focusable element. |
ArrowDown (Optional APG Enhancement) |
Moves focus to the next accordion header button. |
ArrowUp (Optional APG Enhancement) |
Moves focus to the previous accordion header button. |
Home / End (Optional APG) |
Moves focus to the first or last accordion header button. |
Native HTML5 <details> vs. Custom ARIA Accordion
Modern HTML provides the native <details> and <summary> disclosure element:
<!-- Native HTML5 Disclosure Element -->
<details>
<summary>What is your refund policy?</summary>
<p>We offer a 30-day money-back guarantee.</p>
</details>
| Dimension | Native <details> / <summary> |
Custom WAI-ARIA Accordion |
|---|---|---|
| JavaScript Required | ❌ Zero JS (Browser handles toggling natively) | ✅ Requires JS for state toggling |
| Heading Hierarchy | Requires nesting <h3><summary> (some browsers have styling quirks) |
Full semantic heading integration (<h3><button>) |
| CSS Animation Support | Limited (improving with modern CSS interpolate-size) |
Full control over JavaScript height animations |
| Exclusive Accordion Grouping | Requires name="group-name" attribute (HTML 2024+) |
Custom JS logic for single-open vs multi-open behavior |
💻 Interactive Code Playground
Starter Code: Production-Grade Accessible FAQ Accordion
Line-by-Line Code Breakdown
- Lines 49–57 (
<h2><button ...>): Semantic<h2>preserves the document outline; the inner<button>provides native focusability, keyboard triggers (Space/Enter), and ARIA bindings. - Line 52 (
aria-expanded="true"): Informs assistive technology that the first item is open by default. - Line 53 (
aria-controls="acc-panel-1"): Explicitly pairs the toggle button with the panel container. - Line 59 (
role="region" id="acc-panel-1" aria-labelledby="acc-btn-1"): Exposes the panel as a region landmark labeled by the header button. - Line 55 (
aria-hidden="true"on the arrow icon): Hides the visual downward arrow character▼so screen readers don't read "down pointing triangle" aloud. - Lines 108–137 (JavaScript Toggle Logic): Inverts
aria-expandedand toggles the HTML5hiddenattribute synchronously on click.
Expected Browser Render Output
(Clicking any header toggles its panel and updates aria-expanded immediately).
Frequently Asked Questions
─────────────────────────────────────────────────────────────
How do I invite team members to my organization? ▲
Navigate to Settings > Organization > Members and click...
─────────────────────────────────────────────────────────────
What payment methods do you accept? ▼
─────────────────────────────────────────────────────────────
Can I export my analytics data? ▼
─────────────────────────────────────────────────────────────🏋️ Hands-On Exercise
🎯 The Challenge: Build an Accessible Multi-Step Checkout Accordion
Build an accessible 3-step checkout accordion featuring:
- Step 1: Shipping Address
- Step 2: Delivery Options
- Step 3: Payment Details
Instructions:
- Wrap each step header in an
<h3>tag containing a<button type="button">. - Connect each header button to its respective panel using
aria-controlsandaria-expanded. - Give each panel
role="region"andaria-labelledbyreferencing its header button ID. - Ensure Step 1 is expanded initially, while Steps 2 and 3 are collapsed with the
hiddenattribute. - Write JavaScript to toggle panels on button click.
🏁 Starter Code Sandbox
⚠️ Common Pitfalls
- Putting
role="button"Directly on the Heading: Writing<h2 role="button">. This strips the<h2>level from the document outline. Always nest<button>inside<h2>. - Using CSS
opacity: 0orheight: 0Withoutdisplay: noneorvisibility: hidden: If a collapsed panel is merely visually invisible, keyboard users will still tab into the invisible inputs and links inside it. Always applyhiddenordisplay: nonewhen collapsed. - Missing
aria-controlsLinkage: Forgettingaria-controls="panel-id"on the accordion button prevents assistive technologies from offering direct jumping between the header and panel.
💡 Pro Tips
- Exclusive Accordions: To create an accordion where opening one panel automatically closes all others, loop through all other buttons in your click handler and set
aria-expanded="false"andhiddenbefore opening the targeted panel. - Deep-Linking via URL Hash: Parse
window.location.hashon page load. If the hash matches an accordion panel ID, automatically expand that section and scroll it into view.
📌 Key Takeaways
- The WAI-ARIA Accordion pattern requires nesting native
<button>elements inside semantic heading tags (<h2>-<h6>). aria-expanded="true|false"on the trigger button communicates the disclosure state.aria-controlslinks the trigger button to the collapsible content panel's ID.- Collapsible panels must use
role="region"witharia-labelledbylinking back to the header button. - Inactive panels must be hidden using the
hiddenattribute ordisplay: noneto prevent keyboard focus leaks. - --