Frontend Machine Coding: Build a Dynamic Accordion Component

Learn how to build a flexible accordion component with smooth animations, accessibility features, and configurable behavior using React

Frontend Machine Coding: Build a Dynamic Accordion Component

Problem Statement

Build a reusable accordion component that can handle multiple collapsible sections with smooth animations, accessibility features, and configurable behavior. This is a common UI pattern used in FAQs, navigation menus, and content organization.

Requirements

Must-Have Features:

  • Multiple accordion sections that can expand/collapse
  • Smooth animations for opening and closing
  • Only one section open at a time (accordion behavior)
  • Keyboard navigation support (Arrow keys, Enter, Space)
  • Accessible with proper ARIA attributes
  • Configurable content for each section
  • Responsive design

Nice-to-Have Features:

  • Option to allow multiple sections open simultaneously
  • Custom animations and transitions
  • Section icons and badges
  • Lazy loading for content
  • Theme customization
  • Section state persistence

Understanding the Problem

What is an Accordion Component?

An accordion component is a UI pattern that:

  • Organizes content into collapsible sections
  • Provides a space-efficient way to display large amounts of content
  • Improves user experience by reducing cognitive load
  • Follows accessibility guidelines for keyboard and screen reader users

Key Concepts to Implement:

  1. State Management: Track which sections are open/closed
  2. Animation System: Smooth height transitions
  3. Accessibility: ARIA attributes and keyboard navigation
  4. Component Architecture: Reusable and extensible design
  5. Performance: Efficient rendering and animation

Solution Architecture

Component Structure:

Accordion/
├── AccordionContainer (Main wrapper)
├── AccordionItem (Individual section)
│   ├── AccordionHeader (Clickable header)
│   └── AccordionContent (Collapsible content)
└── AccordionProvider (Context for state management)

Data Flow:

  1. Configuration defines accordion sections
  2. AccordionContainer manages overall state
  3. AccordionItem handles individual section behavior
  4. Context provides state to all child components

Complete Solution

Step 1: Define Interfaces and Types

First, let's define the structure for our accordion component:

// Accordion item interface
export interface AccordionItem {
  id: string;
  title: string;
  content: React.ReactNode;
  icon?: React.ReactNode;
  disabled?: boolean;
  badge?: string;
}

// Accordion configuration
export interface AccordionConfig {
  items: AccordionItem[];
  allowMultiple?: boolean;
  defaultOpen?: string[];
  animationDuration?: number;
  theme?: 'light' | 'dark';
}

// Context interface
export interface AccordionContextType {
  openItems: Set<string>;
  toggleItem: (id: string) => void;
  isItemOpen: (id: string) => boolean;
  allowMultiple: boolean;
}

Step 2: Create the Accordion Context

// AccordionContext.tsx
import React, { createContext, useContext, useState, useCallback } from 'react';

const AccordionContext = createContext<AccordionContextType | null>(null);

export const useAccordion = () => {
  const context = useContext(AccordionContext);
  if (!context) {
    throw new Error('useAccordion must be used within AccordionProvider');
  }
  return context;
};

interface AccordionProviderProps {
  children: React.ReactNode;
  allowMultiple?: boolean;
  defaultOpen?: string[];
}

export const AccordionProvider: React.FC<AccordionProviderProps> = ({
  children,
  allowMultiple = false,
  defaultOpen = []
}) => {
  const [openItems, setOpenItems] = useState<Set<string>>(new Set(defaultOpen));

  const toggleItem = useCallback((id: string) => {
    setOpenItems(prev => {
      const newSet = new Set(prev);
      if (newSet.has(id)) {
        newSet.delete(id);
      } else {
        if (!allowMultiple) {
          newSet.clear();
        }
        newSet.add(id);
      }
      return newSet;
    });
  }, [allowMultiple]);

  const isItemOpen = useCallback((id: string) => {
    return openItems.has(id);
  }, [openItems]);

  return (
    <AccordionContext.Provider
      value={{
        openItems,
        toggleItem,
        isItemOpen,
        allowMultiple
      }}
    >
      {children}
    </AccordionContext.Provider>
  );
};

Step 3: Create the Accordion Container

// AccordionContainer.tsx
import React from 'react';
import { AccordionProvider } from './AccordionContext';
import './Accordion.css';

interface AccordionContainerProps {
  children: React.ReactNode;
  allowMultiple?: boolean;
  defaultOpen?: string[];
  className?: string;
}

export const AccordionContainer: React.FC<AccordionContainerProps> = ({
  children,
  allowMultiple = false,
  defaultOpen = [],
  className = ''
}) => {
  return (
    <AccordionProvider allowMultiple={allowMultiple} defaultOpen={defaultOpen}>
      <div className={`accordion-container ${className}`} role="region" aria-label="Accordion">
        {children}
      </div>
    </AccordionProvider>
  );
};

Step 4: Create the Accordion Item

// AccordionItem.tsx
import React, { useState, useRef, useEffect } from 'react';
import { useAccordion } from './AccordionContext';
import { ChevronDownIcon } from './Icons';

interface AccordionItemProps {
  id: string;
  title: string;
  children: React.ReactNode;
  icon?: React.ReactNode;
  disabled?: boolean;
  badge?: string;
}

export const AccordionItem: React.FC<AccordionItemProps> = ({
  id,
  title,
  children,
  icon,
  disabled = false,
  badge
}) => {
  const { isItemOpen, toggleItem } = useAccordion();
  const [contentHeight, setContentHeight] = useState<number>(0);
  const contentRef = useRef<HTMLDivElement>(null);
  const isOpen = isItemOpen(id);

  useEffect(() => {
    if (contentRef.current) {
      setContentHeight(isOpen ? contentRef.current.scrollHeight : 0);
    }
  }, [isOpen, children]);

  const handleKeyDown = (event: React.KeyboardEvent) => {
    if (disabled) return;

    switch (event.key) {
      case 'Enter':
      case ' ':
        event.preventDefault();
        toggleItem(id);
        break;
      case 'ArrowDown':
        event.preventDefault();
        // Navigate to next item
        break;
      case 'ArrowUp':
        event.preventDefault();
        // Navigate to previous item
        break;
    }
  };

  return (
    <div className="accordion-item">
      <button
        className={`accordion-header ${isOpen ? 'open' : ''} ${disabled ? 'disabled' : ''}`}
        onClick={() => !disabled && toggleItem(id)}
        onKeyDown={handleKeyDown}
        aria-expanded={isOpen}
        aria-disabled={disabled}
        aria-controls={`accordion-content-${id}`}
        id={`accordion-header-${id}`}
      >
        <div className="accordion-header-content">
          {icon && <span className="accordion-icon">{icon}</span>}
          <span className="accordion-title">{title}</span>
          {badge && <span className="accordion-badge">{badge}</span>}
        </div>
        <ChevronDownIcon className={`accordion-chevron ${isOpen ? 'rotated' : ''}`} />
      </button>
      
      <div
        className="accordion-content-wrapper"
        style={{ height: `${contentHeight}px` }}
      >
        <div
          ref={contentRef}
          id={`accordion-content-${id}`}
          className="accordion-content"
          role="region"
          aria-labelledby={`accordion-header-${id}`}
        >
          {children}
        </div>
      </div>
    </div>
  );
};

Step 5: Create the Icons Component

// Icons.tsx
import React from 'react';

interface IconProps {
  className?: string;
}

export const ChevronDownIcon: React.FC<IconProps> = ({ className = '' }) => (
  <svg
    className={className}
    width="16"
    height="16"
    viewBox="0 0 16 16"
    fill="none"
    xmlns="http://www.w3.org/2000/svg"
  >
    <path
      d="M4 6L8 10L12 6"
      stroke="currentColor"
      strokeWidth="2"
      strokeLinecap="round"
      strokeLinejoin="round"
    />
  </svg>
);

Step 6: Create the CSS Styles

/* Accordion.css */
.accordion-container {
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  overflow: hidden;
  background: white;
}

.accordion-item {
  border-bottom: 1px solid #e2e8f0;
}

.accordion-item:last-child {
  border-bottom: none;
}

.accordion-header {
  width: 100%;
  padding: 16px 20px;
  background: none;
  border: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: space-between;
  transition: background-color 0.2s ease;
  text-align: left;
}

.accordion-header:hover:not(.disabled) {
  background-color: #f8fafc;
}

.accordion-header:focus {
  outline: 2px solid #3b82f6;
  outline-offset: -2px;
}

.accordion-header.disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.accordion-header-content {
  display: flex;
  align-items: center;
  gap: 12px;
  flex: 1;
}

.accordion-icon {
  display: flex;
  align-items: center;
  color: #64748b;
}

.accordion-title {
  font-weight: 500;
  color: #1e293b;
  font-size: 16px;
}

.accordion-badge {
  background: #3b82f6;
  color: white;
  padding: 2px 8px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: 500;
}

.accordion-chevron {
  transition: transform 0.2s ease;
  color: #64748b;
}

.accordion-chevron.rotated {
  transform: rotate(180deg);
}

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

.accordion-content {
  padding: 0 20px 20px;
  color: #475569;
  line-height: 1.6;
}

/* Dark theme */
.accordion-container.dark {
  background: #1e293b;
  border-color: #334155;
}

.accordion-container.dark .accordion-item {
  border-color: #334155;
}

.accordion-container.dark .accordion-header:hover:not(.disabled) {
  background-color: #334155;
}

.accordion-container.dark .accordion-title {
  color: #f1f5f9;
}

.accordion-container.dark .accordion-content {
  color: #cbd5e1;
}

Step 7: Create the Main App Component

// App.tsx
import React from 'react';
import { AccordionContainer } from './AccordionContainer';
import { AccordionItem } from './AccordionItem';

const accordionData = [
  {
    id: 'section-1',
    title: 'Getting Started',
    content: (
      <div>
        <p>Welcome to our platform! This section will help you get started with the basics.</p>
        <ul>
          <li>Create your account</li>
          <li>Complete your profile</li>
          <li>Explore the dashboard</li>
        </ul>
      </div>
    ),
    badge: 'New'
  },
  {
    id: 'section-2',
    title: 'Advanced Features',
    content: (
      <div>
        <p>Discover advanced features that will enhance your experience.</p>
        <div style={{ background: '#f1f5f9', padding: '12px', borderRadius: '6px' }}>
          <strong>Pro Tips:</strong>
          <ul>
            <li>Use keyboard shortcuts for faster navigation</li>
            <li>Customize your workspace settings</li>
            <li>Enable notifications for important updates</li>
          </ul>
        </div>
      </div>
    )
  },
  {
    id: 'section-3',
    title: 'Troubleshooting',
    content: (
      <div>
        <p>Having issues? Check out these common solutions:</p>
        <details>
          <summary>Connection Problems</summary>
          <p>Try refreshing your browser or clearing cache.</p>
        </details>
        <details>
          <summary>Performance Issues</summary>
          <p>Close unnecessary tabs and clear browser data.</p>
        </details>
      </div>
    )
  }
];

const App: React.FC = () => {
  return (
    <div style={{ maxWidth: '600px', margin: '40px auto', padding: '0 20px' }}>
      <h1>FAQ Accordion</h1>
      
      <AccordionContainer defaultOpen={['section-1']}>
        {accordionData.map((item) => (
          <AccordionItem
            key={item.id}
            id={item.id}
            title={item.title}
            badge={item.badge}
          >
            {item.content}
          </AccordionItem>
        ))}
      </AccordionContainer>

      <h2 style={{ marginTop: '40px' }}>Multiple Open Sections</h2>
      
      <AccordionContainer allowMultiple={true}>
        {accordionData.map((item) => (
          <AccordionItem
            key={`multi-${item.id}`}
            id={`multi-${item.id}`}
            title={item.title}
            badge={item.badge}
          >
            {item.content}
          </AccordionItem>
        ))}
      </AccordionContainer>
    </div>
  );
};

export default App;

Performance Optimizations

1. Memoization

const AccordionItem = React.memo<AccordionItemProps>(({ id, title, children, ...props }) => {
  // Component implementation
});

2. Lazy Loading

const LazyAccordionContent = React.lazy(() => import('./AccordionContent'));

// Use in AccordionItem
{isOpen && (
  <Suspense fallback={<div>Loading...</div>}>
    <LazyAccordionContent>{children}</LazyAccordionContent>
  </Suspense>
)}

3. Virtual Scrolling for Large Lists

import { FixedSizeList as List } from 'react-window';

const VirtualizedAccordion = ({ items }: { items: AccordionItem[] }) => {
  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
    <div style={style}>
      <AccordionItem {...items[index]} />
    </div>
  );

  return (
    <List
      height={400}
      itemCount={items.length}
      itemSize={60}
      width="100%"
    >
      {Row}
    </List>
  );
};

Advanced Features

1. Custom Animations

// Custom animation hook
const useAccordionAnimation = (isOpen: boolean) => {
  const [height, setHeight] = useState(0);
  const contentRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (contentRef.current) {
      const targetHeight = isOpen ? contentRef.current.scrollHeight : 0;
      setHeight(targetHeight);
    }
  }, [isOpen]);

  return { height, contentRef };
};

2. Theme Support

// Theme context
interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');
  
  const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light');
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      <div className={`theme-${theme}`}>
        {children}
      </div>
    </ThemeContext.Provider>
  );
};

3. Section State Persistence

// Local storage hook
const useAccordionPersistence = (key: string) => {
  const [openItems, setOpenItems] = useState<Set<string>>(() => {
    const saved = localStorage.getItem(key);
    return saved ? new Set(JSON.parse(saved)) : new Set();
  });

  const updateOpenItems = useCallback((newItems: Set<string>) => {
    setOpenItems(newItems);
    localStorage.setItem(key, JSON.stringify([...newItems]));
  }, [key]);

  return { openItems, updateOpenItems };
};

Key Takeaways

What We Learned:

  1. Component Architecture: How to design reusable, composable components
  2. State Management: Using React Context for shared state
  3. Accessibility: Implementing proper ARIA attributes and keyboard navigation
  4. Animation: Creating smooth height transitions with CSS
  5. Performance: Optimizing renders and animations

Best Practices:

  • Always consider accessibility from the start
  • Use semantic HTML and ARIA attributes
  • Implement keyboard navigation
  • Test with screen readers
  • Optimize animations for performance
  • Make components configurable and reusable

Common Pitfalls to Avoid:

  • Forgetting to handle keyboard events
  • Not providing proper ARIA attributes
  • Using display: none instead of height animations
  • Not testing with assistive technologies
  • Making components too rigid and non-configurable

Additional Resources


This accordion component demonstrates essential frontend development skills including component architecture, state management, accessibility, and performance optimization. It's a great foundation for building more complex UI components and understanding real-world development patterns.