This guide walks you through building an accessible accordion component in JavaScript that works with the keyboard, screen readers, and mouse users alike. You’ll end up with a robust disclosure widget that follows the WAI-ARIA Authoring Practices, so it behaves predictably in assistive technology. No frameworks are required—just a solid understanding of HTML, CSS, and vanilla JavaScript.
What You’ll Need
- A code editor (VS Code, Sublime Text, or any text editor)
- A modern web browser (Chrome, Firefox, Safari, Edge) for testing
- Basic familiarity with HTML, CSS, and JavaScript (DOM manipulation, event listeners)
- Optional: a screen reader (NVDA, VoiceOver) for testing accessibility
Step-by-Step
- Structure the HTML semantically. Use a
<h3>(or any heading level that fits your document outline) wrapping a<button>for the accordion header, and a<div>for the panel content. The button must be the only interactive element inside the heading. Example:<h3><button aria-expanded="false">Section Title</button></h3><div role="region">...content...</div> - Add ARIA attributes to the button and panel. Give the button
aria-expanded="false"(will toggle totrue) andaria-controls="panel-id"pointing to the panel’sid. On the panel, setrole="region",aria-labelledby="button-id", and give it a uniqueid. - Write the CSS for the collapsed state. Hide the panel with
display: noneor use a class like.accordion-panel[hidden]. Ensure the button has a visible focus style (e.g.,:focus-visible { outline: 2px solid blue; }) for keyboard users. - Add CSS for the expanded state. When the panel is open, remove the hidden state (e.g.,
.accordion-panel:not([hidden]) { display: block; }). Consider adding a transition for a smooth height animation, but avoid animatingdisplay—usemax-heightorgrid-template-rowstrick if needed. - Write the JavaScript to toggle the accordion. Select all buttons with a class like
.accordion-button. Add a click event listener to each. On click, toggle thearia-expandedattribute and thehiddenattribute on the corresponding panel. Update the button’s visual state (e.g., add a class for styling). - Implement keyboard support. The native
<button>already handles Enter and Space. For arrow-key navigation between accordion headers, add akeydownlistener to each button. When ArrowDown or ArrowUp is pressed, move focus to the next or previous button in the list (usingfocus()). - Add Home and End key support. In the same keydown handler, make Home focus the first button and End focus the last. This matches common accordion patterns.
- Handle focus management for the panel. If the panel contains focusable elements (links, inputs), you may want to move focus into the panel when it opens (optional but recommended for long content). Use
panel.querySelector('a, button, input')?.focus(). - Test with a screen reader. Verify that the button announces “expanded” or “collapsed” and that the panel is associated correctly. Use the virtual cursor to navigate and ensure the accordion doesn’t trap focus.
- Test with keyboard only. Tab to the buttons, press Enter/Space to toggle, and use arrow keys to move between headers. Ensure focus is always visible.
Common Mistakes
- Using
<div>with a click handler instead of a<button>—this breaks keyboard support and screen reader semantics. - Forgetting to set
aria-controlsandaria-labelledby—screen readers rely on these to announce relationships. - Hiding the panel with
visibility: hiddenbut not removing it from the accessibility tree—usehiddenattribute ordisplay: none. - Not updating
aria-expandedwhen toggling—this is the primary signal for assistive tech. - Making the entire heading clickable—only the button should be interactive; the heading is just a container.
- Ignoring focus styles—keyboard users need a clear visual indicator of where they are.
Tips
- Use the
hiddenattribute on the panel by default, and remove it when expanded. This ensures the panel is hidden from both visual and assistive tech users. - Keep the accordion button text concise and descriptive. Avoid saying “click here”—use the section title itself.
- If you have multiple accordions on a page, consider whether they should be independent or mutually exclusive (only one open at a time). For exclusive accordions, close others when one opens, but allow the user to close the open one by clicking again.
- For smooth animations, use CSS transitions on
max-heightorgrid-template-rowswith a class toggle, but ensure the content is still accessible during animation. - Test with real assistive technology—automated tools can miss issues like focus order or announcement quirks.
Troubleshooting
- Keyboard events not firing: Ensure the
keydownlistener is attached to the button, not the heading. Also check that the button is not disabled (disabled buttons don’t fire events). - Screen reader announces wrong state: Double-check that
aria-expandedis on the button and its value is updated correctly. Also verify that the panel hasrole="region"andaria-labelledbypointing to the button’s id. - Panel not hiding: If you’re using the
hiddenattribute, make sure no CSS rule overrides it (e.g.,display: block !important). Use the browser’s dev tools to inspect the element and see if the attribute is present. - Arrow keys don’t move focus: The buttons must be siblings within the same parent for
nextElementSiblingorpreviousElementSiblingto work. If they’re nested in separate containers, query all buttons and use index-based navigation. - Focus jumps unexpectedly: When opening a panel, if you move focus into it, ensure you don’t do so on every toggle—only when opening. Use a flag or check
aria-expandedbefore focusing. - Content is not readable by screen reader: Make sure the panel is not
aria-hiddenor haveinertattribute. Thehiddenattribute is sufficient.
Frequently Asked Questions
- Should I use a
<details>element instead? Yes, native<details>provides built-in keyboard support and accessibility, but it lacks some styling control and doesn’t support arrow-key navigation between multiple elements. If you need a custom look or behavior, a button-based accordion is better. - How do I make the accordion open by default? Set
aria-expanded="true"and remove thehiddenattribute from the panel in the HTML. The JavaScript should respect this initial state and not override it on load. - Can I use CSS-only for the accordion? You can use
:focus-withinor:targetfor simple cases, but you lose the ability to toggle with a click (only focus toggles) and you can’t easily manage multiple open panels. JavaScript gives you full control. - What if I have many accordions on a page? The same code works for all, but ensure each button has a unique
idand each panel a uniqueid. Use a class to group them and attach listeners to all. - Does the accordion work on mobile touch devices? Yes, click events work on touch. Ensure the button is large enough (at least 44px) for touch targets, and consider using
touch-action: manipulationto avoid double-tap zoom delays. - How do I animate the height smoothly? Use a CSS grid trick: set
grid-template-rows: 0frto1fron the panel with a transition. Alternatively, measure the scroll height in JavaScript and animate max-height. Avoid animatingdisplay.
Related
General steps for current mainstream versions; menus can differ by device and version. See the disclaimer or report an error.
More in this bed
- Gmail Gemini Privacy: Data Access & Limit Settings
- Fix Outlook Inbox Corruption with DISM in Windows
- How to Set Up Apple Pay on iPhone
- How to Choose a Refurbished iPhone With Years of Support
- Voice Dictation in Google Docs: How to Use It
- Turn Off Message Notifications on Mac