#Accordion#React

Accordion

Learn to build an accessible accordion component in React with smooth animations, keyboard navigation, and single or multi-expand modes. Perfect for FAQs and collapsible UI sections.

By Pratik RaiMedium

Build an accordion component that allows users to expand and collapse content sections. The key challenges are managing state for multiple accordion items, implementing smooth animations, ensuring accessibility with keyboard navigation, and handling edge cases like preventing multiple items from being open simultaneously (if needed).

The task

Goal: build an accordion that expands and collapses sections of content.

Requirements

  1. Render a list of sections, each with a header and a hidden panel.
  2. Clicking a header expands its panel; clicking it again collapses it.
  3. Support a single-open mode, where opening one section closes the others.
  4. Make the headers operable by keyboard.
  5. Animate the open and close rather than snapping.

Stretch goals

  • Arrow-key navigation between headers.
  • Allow a section to be open on first render.
  • Support arbitrary content, not just text, inside a panel.

Hints

  1. For single-open mode, hold the id of the open section. For multi-open, hold a set of ids. The mode is a prop, not two components.
  2. Use a real button for each header so it is focusable and announced correctly, and toggle aria-expanded on it.
  3. Animating to height: auto does not work. Animate max-height to a value large enough, or measure the content and animate to a pixel height.
  4. Collapsed content should be genuinely hidden rather than merely invisible, or a screen reader will read sections the user thinks are closed.

Overview

An accordion is a UI component that displays a list of items where each item can be expanded to reveal its content or collapsed to hide it. This pattern is commonly used for FAQs, navigation menus, and organizing content into collapsible sections. According to the W3C WAI-ARIA Authoring Practices, an accordion is a vertically stacked set of interactive headings that each contain a title representing a section of content.

Architecture Overview

The implementation uses a controlled component pattern with centralized state management:

┌─────────────────────────────────────┐
│  AccordionDemo (Parent)             │
│  - Provides data array              │
│  - Configures singleOpen mode       │
└──────────────┬──────────────────────┘
               │
               ▼
┌─────────────────────────────────────┐
│  AccordionComponent                 │
│  - Manages openItems state          │
│  - Handles keyboard navigation      │
│  - Manages button refs              │
│  - Renders accordion items          │
└─────────────────────────────────────┘

Key Components:

  1. State Array: Tracks which items are currently open using an array of IDs
  2. Refs Map: Stores references to all button elements for keyboard navigation
  3. Toggle Logic: Handles single vs. multiple open modes
  4. Keyboard Handler: Implements ARIA-compliant keyboard navigation

Core Challenge: State Management

The primary challenge is managing which accordion items are open or closed, especially when dealing with multiple items and different behaviors (single vs. multiple items open at once). We'll use React's useState hook for this purpose.

State Structure

The component uses an array to track open items:

TSXcomponent.tsx
1const [openItems, setOpenItems] = useState<number[]>([]);

Why an array?

  • Supports both single and multiple open modes
  • Easy to check if an item is open: openItems.includes(item.id)
  • Simple to add/remove items
  • Can be extended to support default open items

Single vs. Multiple Open Items

The component supports two modes controlled by the singleOpen prop:

Single Open Mode (Default):

TSXcomponent.tsx
1if (singleOpen) { 2 return prev[0] === id ? [] : [id]; 3}
  • Only one item can be open at a time
  • Clicking an open item closes it
  • Clicking a closed item opens it and closes any previously open item

Multiple Open Mode:

TSXcomponent.tsx
1else { 2 return prev.includes(id) 3 ? prev.filter(openId => openId !== id) 4 : [...prev, id]; 5}
  • Multiple items can be open simultaneously
  • Each item toggles independently
  • Users can view multiple sections at once

Implementation: State Management

Toggle Handler

TSXcomponent.tsx
1const handleOpen = (id: number) => { 2 setOpenItems(prev => { 3 if (singleOpen) { 4 // Single mode: replace array with new item or empty 5 return prev[0] === id ? [] : [id]; 6 } else { 7 // Multiple mode: add or remove from array 8 return prev.includes(id) 9 ? prev.filter(openId => openId !== id) 10 : [...prev, id]; 11 } 12 }); 13}

Key Logic:

  • Single mode: If clicking the currently open item, close it ([]). Otherwise, open only that item ([id]).
  • Multiple mode: If item is open, remove it. If closed, add it to the array.

Keyboard Navigation

The component implements full keyboard navigation following ARIA best practices:

Supported Keys

  1. Enter / Space: Toggle the current panel
  2. ArrowDown: Move focus to next accordion header
  3. ArrowUp: Move focus to previous accordion header
  4. Home: Move focus to first accordion header
  5. End: Move focus to last accordion header

Implementation

TSXcomponent.tsx
1const handleKeyDown = (e: React.KeyboardEvent<HTMLButtonElement>, id: number, index: number) => { 2 const key = e.key; 3 const keyCode = e.keyCode || e.which; 4 5 // Enter or Space: Toggle the panel 6 if (key === 'Enter' || key === ' ' || keyCode === 13 || keyCode === 32) { 7 e.preventDefault(); 8 handleOpen(id); 9 return; 10 } 11 12 // Arrow keys: Navigate between headers 13 if (key === 'ArrowDown' || keyCode === 40) { 14 e.preventDefault(); 15 const nextIndex = index < data.length - 1 ? index + 1 : 0; 16 const nextButton = buttonRefs.current[data[nextIndex].id]; 17 nextButton?.focus(); 18 return; 19 } 20 21 // ... similar for ArrowUp, Home, End 22}

Focus Management with Refs

The component uses a refs map to store references to all button elements. Learn more about useRef in the React documentation:

TSXcomponent.tsx
1const buttonRefs = useRef<{ [key: number]: HTMLButtonElement | null }>({}); 2 3// Store ref when rendering 4<button 5 ref={(el) => { buttonRefs.current[item.id] = el; }} 6 // ... 7>

Why use refs?

  • Direct DOM access for focus() calls
  • No re-renders needed for focus changes
  • Efficient keyboard navigation
  • Works with dynamic item lists

Circular Navigation

Arrow keys implement circular navigation:

  • ArrowDown on last item → focuses first item
  • ArrowUp on first item → focuses last item

This creates a seamless keyboard experience where users can cycle through all items.

Accessibility Features

The component implements comprehensive ARIA attributes and semantic HTML:

ARIA Attributes

TSXcomponent.tsx
1<button 2 id={buttonId} 3 aria-expanded={isOpen} 4 aria-controls={panelId} 5 // ... 6>
  • aria-expanded: Indicates whether the panel is open (true) or closed (false)
  • aria-controls: Links the button to the panel it controls
  • id: Unique identifier for the button
TSXcomponent.tsx
1<div 2 id={panelId} 3 role="region" 4 aria-labelledby={buttonId} 5 hidden={!isOpen} 6 // ... 7>

Semantic HTML

  • Uses <button> for clickable headers (not <div>)
  • Uses semantic structure with proper IDs
  • Content is properly hidden when collapsed

Component Structure

Props Interface

TSXcomponent.tsx
1interface AccordionItem { 2 id: number; 3 title: string; 4 content: string; 5} 6 7interface AccordionComponentProps { 8 data: AccordionItem[]; 9 singleOpen?: boolean; 10}

Rendering Logic

TSXcomponent.tsx
1{data.map((item, index) => { 2 const buttonId = `accordion-button-${item.id}`; 3 const panelId = `accordion-panel-${item.id}`; 4 const isOpen = openItems.includes(item.id); 5 6 return ( 7 <div key={item.id} className='accordion-item'> 8 <button 9 ref={(el) => { buttonRefs.current[item.id] = el; }} 10 id={buttonId} 11 onClick={() => handleOpen(item.id)} 12 onKeyDown={(e) => handleKeyDown(e, item.id, index)} 13 aria-expanded={isOpen} 14 aria-controls={panelId} 15 > 16 {item.title} 17 <ChevronDownIcon className={isOpen ? 'open' : ''} /> 18 </button> 19 <div 20 id={panelId} 21 role="region" 22 aria-labelledby={buttonId} 23 hidden={!isOpen} 24 > 25 <p>{item.content}</p> 26 </div> 27 </div> 28 ); 29})}

Key Points:

  • Unique IDs generated for each item
  • isOpen calculated from state array
  • Refs stored during render
  • ARIA attributes dynamically set
  • Content hidden when not open

Visual Feedback

Icon Animation

The chevron icon rotates when an item is opened:

CSSstyles.css
1.accordion-item-header-icon { 2 transition: transform 0.3s ease; 3} 4 5.accordion-item-header-icon-open { 6 transform: rotate(180deg); 7}

This provides clear visual feedback about the accordion's state.

Edge Cases Handled

  1. Rapid Clicking: State updates are batched by React, preventing race conditions
  2. Keyboard Navigation: All navigation keys are handled, preventing default browser behavior
  3. Focus Management: Refs ensure focus moves correctly even with dynamic content
  4. Circular Navigation: Arrow keys wrap around at boundaries
  5. Empty State: Component handles empty data arrays gracefully
  6. Toggle Behavior: Clicking an open item in single mode closes it (toggle)

Usage Example

TSXcomponent.tsx
1const data = [ 2 { id: 1, title: 'Section 1', content: 'Content 1' }, 3 { id: 2, title: 'Section 2', content: 'Content 2' }, 4 { id: 3, title: 'Section 3', content: 'Content 3' }, 5]; 6 7// Single open mode (default) 8<AccordionComponent data={data} /> 9 10// Multiple open mode 11<AccordionComponent data={data} singleOpen={false} />

Performance Considerations

  1. State Updates: Using functional updates (prev => ...) ensures correct state batching
  2. Refs: Direct DOM access avoids unnecessary re-renders
  3. Hidden Attribute: Using hidden instead of conditional rendering keeps DOM structure stable
  4. Event Handlers: Inline handlers are acceptable for this use case, but could be memoized with useCallback if needed

Best Practices Demonstrated

  1. Controlled Component: Parent controls data, component manages UI state
  2. Accessibility First: Full keyboard support and ARIA attributes
  3. Flexible API: Supports both single and multiple open modes
  4. Type Safety: TypeScript interfaces ensure correct prop types
  5. Semantic HTML: Uses appropriate HTML elements and roles
  6. Visual Feedback: Icon rotation provides clear state indication

What interviewers look for

  • Are the headers buttons? A div with an onClick is not reachable by keyboard and has no role. This is the single most common miss.
  • Does aria-expanded reflect the state? It is one attribute, and it is the difference between a component that works with assistive technology and one that only looks like it does.
  • How did you handle the height animation? Everyone hits height: auto not being animatable; interviewers want to hear that you know why, not just that you worked around it.
  • Is single-open a mode or a fork? Two nearly identical components is the answer that suggests the state was not thought through.

Goal: Implement an accordion component that can be used to display a list of items.

Frequently asked questions

Is the accordion a common frontend interview question?
It is a frequent warm-up in UI rounds. The visible behaviour takes minutes, which is exactly why it is asked — the interviewer is watching what you do about keyboard support, ARIA state, and the fact that height cannot be animated to `auto`.
Should an accordion allow multiple sections open at once?
Both behaviours are legitimate, so treat it as a prop rather than a decision. Single-open holds the id of the open section; multi-open holds a set of ids. Building two near-identical components instead is the answer that suggests the state was not thought through.
Why can't you animate height to auto?
Because `auto` is not a number, so the browser has nothing to interpolate towards. The usual workarounds are animating `max-height` to a value comfortably larger than the content, or measuring the panel and animating to an explicit pixel height. Knowing why it fails matters more here than which workaround you pick.
What accessibility attributes does an accordion need?
Each header should be a real button, carry `aria-expanded` reflecting its state, and point at its panel with `aria-controls`. Collapsed panels should be genuinely hidden rather than visually hidden, so a screen reader does not read out sections the user believes are closed.

Related Challenges

Continue learning with these related challenges

View All
React

Image Carousel

Create an interactive image carousel in React with smooth slide transitions, navigation arrows, dot indicators, autoplay, and touch/swipe support for mobile devices.

React · JavaScriptPratik Rai ·

React

Dynamic Tic Tac Toe

Build a dynamic Tic Tac Toe game in React with customizable grid sizes, win detection algorithms, player turn management, and game reset functionality. Great for interviews.

React · JavaScriptPratik Rai ·

React

Dice Roller

Create an animated dice roller component in React with realistic rolling animations, random number generation, multiple dice support, and roll history tracking.

React · JavaScriptPratik Rai ·