sodcalculator

10 Tech How-To

Accessible JavaScript Accordion with Keyboard Support

Guide 10-21323 · Updated · Step by step

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

  1. 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>
  2. Add ARIA attributes to the button and panel. Give the button aria-expanded="false" (will toggle to true) and aria-controls="panel-id" pointing to the panel’s id. On the panel, set role="region", aria-labelledby="button-id", and give it a unique id.
  3. Write the CSS for the collapsed state. Hide the panel with display: none or 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.
  4. 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 animating display—use max-height or grid-template-rows trick if needed.
  5. 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 the aria-expanded attribute and the hidden attribute on the corresponding panel. Update the button’s visual state (e.g., add a class for styling).
  6. Implement keyboard support. The native <button> already handles Enter and Space. For arrow-key navigation between accordion headers, add a keydown listener to each button. When ArrowDown or ArrowUp is pressed, move focus to the next or previous button in the list (using focus()).
  7. 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.
  8. 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().
  9. 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.
  10. 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-controls and aria-labelledby—screen readers rely on these to announce relationships.
  • Hiding the panel with visibility: hidden but not removing it from the accessibility tree—use hidden attribute or display: none.
  • Not updating aria-expanded when 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 hidden attribute 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-height or grid-template-rows with 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 keydown listener 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-expanded is on the button and its value is updated correctly. Also verify that the panel has role="region" and aria-labelledby pointing to the button’s id.
  • Panel not hiding: If you’re using the hidden attribute, 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 nextElementSibling or previousElementSibling to 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-expanded before focusing.
  • Content is not readable by screen reader: Make sure the panel is not aria-hidden or have inert attribute. The hidden attribute is sufficient.

Frequently Asked Questions

  1. 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.
  2. How do I make the accordion open by default? Set aria-expanded="true" and remove the hidden attribute from the panel in the HTML. The JavaScript should respect this initial state and not override it on load.
  3. Can I use CSS-only for the accordion? You can use :focus-within or :target for 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.
  4. What if I have many accordions on a page? The same code works for all, but ensure each button has a unique id and each panel a unique id. Use a class to group them and attach listeners to all.
  5. 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: manipulation to avoid double-tap zoom delays.
  6. How do I animate the height smoothly? Use a CSS grid trick: set grid-template-rows: 0fr to 1fr on the panel with a transition. Alternatively, measure the scroll height in JavaScript and animate max-height. Avoid animating display.

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

The whole Tech How-To bed →