How To Build A React Accordion Menu From Scratch

Master the art of building accessible, animated accordion menus with React. From basic state management to WCAG-compliant components, this guide covers everything you need to create professional UI interactions.

Introduction

Accordion menus are one of the most common UI patterns in modern web development. From FAQ sections to product feature breakdowns, accordions help organize content while saving valuable screen space. In this comprehensive guide, you'll learn how to build a fully functional, accessible, and animatable accordion menu using React from scratch.

We'll cover everything from basic implementation with React hooks to advanced features like smooth animations and accessibility compliance. Whether you're building a simple FAQ section or a complex navigation menu, this guide will give you the foundation you need. Building an accordion from scratch also teaches fundamental React concepts that apply across all your projects, including state management with hooks, component composition, event handling, and accessibility patterns that appear throughout React development.

What You'll Learn

Key concepts covered in this guide

React Hooks Fundamentals

Master useState for managing accordion open/close state with clean, predictable code.

Component Architecture

Build reusable Accordion and AccordionItem components that integrate seamlessly into any project.

CSS Styling & Animations

Create smooth transitions and polished visuals with CSS transitions and responsive design.

Accessibility Compliance

Implement WCAG-compliant accordions with ARIA attributes, keyboard navigation, and screen reader support.

Advanced State Patterns

Handle single vs multiple open panels with controlled and uncontrolled component patterns.

Performance Optimization

Memoization techniques and best practices for optimal rendering performance.

Understanding Accordion Components in React

An accordion is a vertically stacked set of interactive headers that expand or collapse to reveal associated content. The term comes from the musical instrument that expands and contracts, and the UI pattern works similarly--panels expand to show content and collapse to hide it.

What Makes a Good Accordion

A well-designed accordion component should have several key characteristics that distinguish it from simple collapsible sections. First, it needs clear visual indicators that communicate interactivity--users should immediately understand they can click to expand or collapse content. Second, it should provide smooth, performant animations that don't feel jarring when content appears or disappears. Third, it must be fully accessible, meaning keyboard users and screen reader users can navigate and interact with the component without barriers.

Common Use Cases for Accordions

Accordions appear throughout web applications for several distinct purposes. FAQ sections represent the most common use case, where questions serve as headers and answers expand to show detailed responses. Product specification pages often use accordions to organize technical details without overwhelming users with information. Navigation menus, particularly on mobile devices, frequently employ accordion patterns to organize hierarchical links while keeping the interface clean. Beyond these common examples, accordions work well for organizing form fields into logical groups, displaying terms and conditions or privacy policies, breaking down long articles into digestible sections, and presenting comparison tables in a space-efficient manner.

Why Build from Scratch

While numerous React libraries provide pre-built accordion components, building from scratch offers significant advantages for your development skills and your applications. Understanding the underlying mechanics helps you debug issues more effectively when problems arise. You gain complete control over the component's behavior, styling, and accessibility features without being constrained by library opinions. The implementation can be exactly tailored to your specific needs rather than working around library limitations.

AccordionItem Component - Basic Implementation
1import { useState } from 'react';2import './AccordionItem.css';3 4function AccordionItem({ title, content, isOpen, onToggle }) {5 return (6 <div className={`accordion-item ${isOpen ? 'open' : ''}`}>7 <button8 className="accordion-header"9 onClick={onToggle}10 aria-expanded={isOpen}11 >12 <span className="accordion-title">{title}</span>13 <span className="accordion-icon" aria-hidden="true">14 {isOpen ? '−' : '+'}15 </span>16 </button>17 {isOpen && (18 <div className="accordion-content">19 <p>{content}</p>20 </div>21 )}22 </div>23 );24}25 26export default AccordionItem;
Main Accordion Container Component
1import { useState } from 'react';2import AccordionItem from './AccordionItem';3import './Accordion.css';4 5function Accordion({ items }) {6 const [openIndex, setOpenIndex] = useState(null);7 8 const handleToggle = (index) => {9 setOpenIndex(openIndex === index ? null : index);10 };11 12 return (13 <div className="accordion">14 {items.map((item, index) => (15 <AccordionItem16 key={index}17 title={item.title}18 content={item.content}19 isOpen={openIndex === index}20 onToggle={() => handleToggle(index)}21 />22 ))}23 </div>24 );25}26 27export default Accordion;
Accordion CSS Styles
1.accordion {2 width: 100%;3 max-width: 600px;4 margin: 0 auto;5 border: 1px solid #e0e0e0;6 border-radius: 8px;7 overflow: hidden;8}9 10.accordion-header {11 width: 100%;12 display: flex;13 justify-content: space-between;14 align-items: center;15 padding: 16px 20px;16 background: #ffffff;17 border: none;18 border-bottom: 1px solid #e0e0e0;19 cursor: pointer;20 font-size: 16px;21 font-weight: 500;22 text-align: left;23 color: #333;24 transition: background-color 0.2s ease;25}26 27.accordion-header:hover {28 background-color: #f5f5f5;29}30 31.accordion-header:focus {32 outline: 2px solid #007bff;33 outline-offset: -2px;34}35 36.accordion-icon {37 font-size: 20px;38 font-weight: 300;39 color: #666;40 transition: transform 0.2s ease;41}42 43.accordion-item.open .accordion-icon {44 transform: rotate(180deg);45}46 47.accordion-content {48 padding: 20px;49 background-color: #fafafa;50 border-bottom: 1px solid #e0e0e0;51 line-height: 1.6;52 color: #555;53}

Implementing Smooth Animations

Animations enhance the user experience by providing visual continuity when panels open and close. CSS transitions offer the simplest approach to adding animation without complex JavaScript calculations.

CSS Transition Approach

The CSS transition method animates properties when panels open and close.

.accordion-content {
 padding: 20px;
 background-color: #fafafa;
 overflow: hidden;
 transition: max-height 0.3s ease-out;
}

.accordion-item.open .accordion-content {
 max-height: 500px;
}

This technique uses max-height to animate the reveal. The element animates from max-height: 0 (implicit) to the specified maximum height. The transition creates the smooth expansion effect.

Enhanced Animation with CSS Variables

For more flexible animations that work regardless of content height, use JavaScript to calculate the exact content height and apply it dynamically.

.accordion-content-wrapper {
 overflow: hidden;
 transition: all 0.3s ease;
}

.accordion-content-wrapper.collapsing {
 height: 0 !important;
}

The CSS-only approach works well for many use cases, but if your content varies significantly in length, you might need JavaScript-based height calculations. As noted in FreeCodeCamp's guide to accordion menus, libraries like react-transition-group or CSS-in-JS solutions can also help manage these animations more elegantly.

Managing Multiple Open Panels

Some accordion implementations allow multiple panels to be open simultaneously, while others restrict expansion to a single panel at a time. The appropriate choice depends on your use case and user expectations.

Implementing Multiple Open Panels

To support multiple open panels, change the state structure to track an array of open indices rather than a single value:

function Accordion({ items, allowMultiple = false }) {
 const [openIndices, setOpenIndices] = useState([]);

 const handleToggle = (index) => {
 if (allowMultiple) {
 setOpenIndices(prev =>
 prev.includes(index)
 ? prev.filter(i => i !== index)
 : [...prev, index]
 );
 } else {
 setOpenIndices(prev =>
 prev.includes(index) ? [] : [index]
 );
 }
 };

 return (
 <div className="accordion">
 {items.map((item, index) => (
 <AccordionItem
 key={index}
 title={item.title}
 content={item.content}
 isOpen={openIndices.includes(index)}
 onToggle={() => handleToggle(index)}
 />
 ))}
 </div>
 );
}

The allowMultiple prop provides flexibility--use true for contexts where users benefit from comparing multiple sections, and false when you want to focus attention on a single section at a time. According to GeeksforGeeks's React accordion tutorial, this approach enables flexible data handling through props while maintaining clean component architecture.

When to Allow Multiple Panels

Consider allowing multiple open panels when users need to compare information across sections, such as comparing product features or pricing tiers. For FAQ sections where users often have multiple questions, allowing multiple open panels improves the user experience by eliminating the need to repeatedly open and close panels.

Ensuring Accessibility

Accessibility isn't optional--it's essential for creating inclusive web experiences. Accordions present specific accessibility challenges that require careful attention to ensure all users can interact with the component effectively.

Keyboard Navigation

Users should be able to navigate through accordion headers using keyboard arrow keys. The Tab key moves focus between interactive elements, and Enter or Space should activate accordion headers. Arrow keys within the accordion can provide additional navigation patterns. Implementing proper keyboard support is a fundamental aspect of accessible web development that ensures your components work for everyone.

Key ARIA Attributes

  • aria-expanded - communicates current state to assistive technologies
  • aria-controls - links the header to its content panel
  • role="region" - identifies the content area as significant
  • hidden - ensures hidden content is properly removed from the accessibility tree

Screen Reader Considerations

Screen reader users need clear, descriptive content to understand the accordion's purpose and current state. Each header should have a meaningful label that describes what will expand, and the content should be structured logically once revealed. Announcements should inform users when panels open and close--some implementations use aria-live regions to announce these state changes.

Accessible AccordionItem Component
1function AccordionItem({ title, content, isOpen, onToggle }) {2 const handleKeyDown = (e) => {3 if (e.key === 'Enter' || e.key === ' ') {4 e.preventDefault();5 onToggle();6 }7 };8 9 return (10 <div className="accordion-item">11 <button12 className="accordion-header"13 onClick={onToggle}14 onKeyDown={handleKeyDown}15 aria-expanded={isOpen}16 aria-controls={`accordion-content-${title.toLowerCase().replace(/\s+/g, '-')}`}17 >18 <span className="accordion-title">{title}</span>19 </button>20 <div21 id={`accordion-content-${title.toLowerCase().replace(/\s+/g, '-')}`}22 role="region"23 aria-labelledby={`accordion-header-${title.toLowerCase().replace(/\s+/g, '-')}`}24 className="accordion-content"25 hidden={!isOpen}26 >27 <p>{content}</p>28 </div>29 </div>30 );31}

Advanced Patterns and Best Practices

Controlled vs Uncontrolled Components

React components can be either controlled or uncontrolled:

Controlled components have state managed entirely by their parent:

function Accordion({ items, openIndices, onToggle }) {
 return (
 <div className="accordion">
 {items.map((item, index) => (
 <AccordionItem
 key={index}
 title={item.title}
 content={item.content}
 isOpen={openIndices.includes(index)}
 onToggle={() => onToggle(index)}
 />
 ))}
 </div>
 );
}

Controlled components give the parent complete control over state, making them easier to integrate with forms, data from APIs, or complex application state. Uncontrolled components manage their own internal state and are simpler to use in isolation but offer less flexibility for complex integrations.

Performance Optimization

For accordions with many items or complex content, performance optimization becomes important. Consider memoizing components to prevent unnecessary re-renders and lazy-loading content that's not immediately visible. The React documentation recommends using React.memo for preventing unnecessary re-renders and useMemo for caching expensive computations.

Integration with Design Systems

When building accordions for production applications, consider how the component fits into your broader design system. Define consistent props for:

  • Size variants: small, medium, large
  • Appearance: borders, shadows, background colors
  • Behavior: animation duration, easing functions

This approach allows the accordion to adapt to different design contexts without code changes, making it more reusable across your application and maintaining consistency in your custom web development projects.

Performance-Optimized Accordion with Memoization
1import { memo, useMemo } from 'react';2 3const AccordionItem = memo(function AccordionItem({ title, content, isOpen, onToggle }) {4 return (5 <div className="accordion-item">6 <button7 className="accordion-header"8 onClick={onToggle}9 aria-expanded={isOpen}10 >11 <span className="accordion-title">{title}</span>12 </button>13 {isOpen && (14 <div className="accordion-content">15 <p>{content}</p>16 </div>17 )}18 </div>19 );20});21 22function Accordion({ items }) {23 const processedItems = useMemo(() =>24 items.map(item => ({25 ...item,26 contentId: item.title.toLowerCase().replace(/\s+/g, '-')27 })),28 [items]29 );30 31 return (32 <div className="accordion">33 {processedItems.map((item, index) => (34 <AccordionItem35 key={item.contentId}36 title={item.title}37 content={item.content}38 isOpen={item.isOpen}39 onToggle={item.onToggle}40 />41 ))}42 </div>43 );44}

Frequently Asked Questions

Conclusion

Building an accordion component from scratch teaches valuable React skills while giving you complete control over functionality and appearance. The implementation we've explored covers the essential patterns: state management with hooks (useState), component composition (Accordion + AccordionItem), accessibility features (ARIA attributes, keyboard navigation), and styling approaches (CSS transitions, responsive design).

Start with the basic implementation and gradually add features as needed:

  1. Begin with functional state management
  2. Add styling and animations
  3. Implement accessibility features
  4. Optimize for performance
  5. Adapt to your design system

The accessibility work might seem like extra effort, but it ensures your components work for everyone. The animation polish might be skipped for simple use cases but adds significant polish for user-facing features.

Remember that the best accordion is one that serves your specific use case. The patterns in this guide provide a foundation--adapt them to fit your project's requirements, design system, and user expectations. For professional assistance with React component development or custom web application development, contact our team to discuss how we can help bring your projects to life.

Ready to Build Professional React Components?

Our team of experienced React developers can help you create custom components, optimize performance, and ensure accessibility compliance for your web applications.

Sources

  1. FreeCodeCamp - How to Build Simpler Accordion Menus with HTML details - Comprehensive guide covering native HTML accordion elements, CSS styling techniques, and accessibility considerations.
  2. GeeksforGeeks - Accordion Template using ReactJS and Tailwind - Complete React implementation demonstrating state management, component structure, and dynamic data handling.