Accordion

An accessible accordion interface with minimal markup.

Single Active Accordion

Lorem ipsum dolor sit amet consectetur adipisicing elit. Accusamus, reprehenderit? This content can be accessed with a screen reader and keyboard navigation.

Lorem ipsum dolor sit amet consectetur adipisicing elit. Accusamus, reprehenderit? This panel shows how aria-expanded changes with the accordion state.

Lorem ipsum dolor sit amet consectetur adipisicing elit. Accusamus, reprehenderit? You can press Enter or Space on the header to toggle this panel.

Multiple Active Accordions

Lorem ipsum dolor sit amet consectetur adipisicing elit. Accusamus, reprehenderit? Multiple accordions can be open simultaneously in this group.

Lorem ipsum dolor sit amet consectetur adipisicing elit. Accusamus, reprehenderit? This example uses checkboxes instead of radio buttons.

Lorem ipsum dolor sit amet consectetur adipisicing elit. Accusamus, reprehenderit? Screen readers will announce the expanded state of each accordion header.

How It Works

This accordion component is built primarily using HTML and CSS, with minimal JavaScript added only for keyboard accessibility.

The implementation relies on hidden radio inputs (for single-active accordions) and checkboxes (for multiple-active accordions) to control the state. The CSS :checked selector and general sibling combinator (~) handle the showing/hiding of content.

Key Accessibility Features

  • Keyboard navigation: All accordion headers are focusable with tabindex="0". Users can press Enter or Space to toggle an accordion (requires JavaScript).
  • ARIA attributes: Proper aria-expanded, aria-controls, and role attributes ensure screen readers can understand the component's state and structure.
  • Focus management: Visual focus indicators help keyboard users understand where they are in the component.
  • Screen reader support: The component structure is semantically correct for optimal screen reader announcements.

Why JavaScript is Necessary

While we've minimized JavaScript usage, there are key limitations with a CSS-only approach:

  1. Keyboard interaction: CSS cannot detect keyboard events like Enter or Space. Only JavaScript can listen for these events and trigger the accordion toggle.
  2. ARIA state updates: CSS cannot dynamically update ARIA attributes like aria-expanded when the accordion state changes.
  3. Non-native elements: Our accordion uses <label> elements, which don't have built-in keyboard interaction like true <button> elements would.

Our JavaScript is minimal (only ~30 lines) and focuses solely on:

  • Handling keyboard events (Enter/Space) to toggle accordions
  • Updating the aria-expanded attribute to match the accordion's state

Without this JavaScript, the accordion would still work for mouse users but would fail accessibility requirements for keyboard-only users.

Accessibility Features

ARIA Attributes Explained

  • role="region": Identifies the container as a landmark region
  • role="button": Indicates that the header acts as a button
  • aria-expanded="true/false": Indicates whether the accordion panel is expanded or collapsed
  • aria-controls="ID": Associates the header with the panel it controls
  • aria-hidden="true": Applied to decorative elements that should be ignored by screen readers
  • tabindex="0": Makes the accordion headers keyboard focusable

Keyboard Navigation

  • Tab key: Moves focus between accordion headers
  • Enter/Space: Toggles the accordion panel when focus is on a header

CSS-only Approach

While JavaScript can enhance the accessibility of accordions, this component demonstrates how to implement core accessibility features without it:

  • Using :focus-visible for visible keyboard focus indicators
  • Structuring HTML to maintain a logical tab order
  • Leveraging ARIA attributes for screen reader announcements
  • Ensuring sufficient color contrast and touch targets

Additional Resources

Implementation Code

HTML

<!-- Single active accordion (radio buttons) -->
<div class="accordion-container" role="region" aria-label="Accordion group">
  <div class="accordion-item">
    <label class="accordion-header" for="accordion-1" tabindex="0" role="button" 
           aria-expanded="false" aria-controls="content-accordion-1">
      <p>Accordion Item 1</p>
      <span class="accordion-icon" aria-hidden="true"></span>
    </label>
    <input type="radio" name="accordion" id="accordion-1" />
    <div class="accordion-content" id="content-accordion-1">
      <p>Content for accordion item 1</p>
    </div>
  </div>
  
  <!-- Additional accordion items with the same structure -->
</div>

<!-- Multiple active accordions (checkboxes) -->
<div class="accordion-container" role="region" aria-label="Multiple accordions">
  <div class="accordion-item">
    <label class="accordion-header" for="m-accordion-1" tabindex="0" role="button" 
           aria-expanded="false" aria-controls="content-m-accordion-1">
      <p>Accordion Item 1</p>
      <span class="accordion-icon" aria-hidden="true"></span>
    </label>
    <input type="checkbox" name="m-accordion" id="m-accordion-1" />
    <div class="accordion-content" id="content-m-accordion-1">
      <p>Content for accordion item 1</p>
    </div>
  </div>
  
  <!-- Additional accordion items with the same structure -->
</div>

Keyboard Accessibility JavaScript (Required)

// This script enables keyboard interaction (Enter/Space) for accordion headers
document.addEventListener('DOMContentLoaded', function() {
  // Get all accordion headers
  const accordionHeaders = document.querySelectorAll('.accordion-header');
  
  // Add keyboard event listeners to all accordion headers
  accordionHeaders.forEach(header => {
    // Handle keyboard events (Enter and Space)
    header.addEventListener('keydown', function(e) {
      // Trigger the accordion when Enter or Space is pressed
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault(); // Prevent scrolling on Space
        
        // Simulate a click on the header
        this.click();
        
        // Get the associated input
        const inputId = this.getAttribute('for');
        const input = document.getElementById(inputId);
        
        // Update aria-expanded state
        const isExpanded = input.checked;
        this.setAttribute('aria-expanded', isExpanded ? 'true' : 'false');
      }
    });
    
    // Also update aria-expanded when clicked with mouse
    header.addEventListener('click', function() {
      // Get the associated input
      const inputId = this.getAttribute('for');
      const input = document.getElementById(inputId);
      
      // We need to wait a moment (10ms, not 10s) for the checked state to update
      // This tiny delay ensures the browser has updated the input's checked state before we read it
      setTimeout(() => {
        const isExpanded = input.checked;
        this.setAttribute('aria-expanded', isExpanded ? 'true' : 'false');
      }, 10);
    });
  });
});