typescriptintermediate

Accordion Component

Accessible accordion component with keyboard navigation and animated expand/collapse.

typescript
import { useState, useRef, ReactNode } from 'react';

interface AccordionItemProps {
  title: string;
  children: ReactNode;
  isOpen: boolean;
  onToggle: () => void;
}

function AccordionItem({ title, children, isOpen, onToggle }: AccordionItemProps) {
  const contentRef = useRef<HTMLDivElement>(null);

  return (
    <div className="border-b border-white/10">
      <button
        className="w-full flex justify-between items-center py-4 text-left"
        onClick={onToggle}
        aria-expanded={isOpen}
      >
        <span className="font-medium">{title}</span>
        <span className={`transform transition-transform ${isOpen ? 'rotate-180' : ''}`}>ā–¼</span>
      </button>
      <div
        ref={contentRef}
        style={{ maxHeight: isOpen ? contentRef.current?.scrollHeight : 0 }}
        className="overflow-hidden transition-all duration-300"
      >
        <div className="pb-4">{children}</div>
      </div>
    </div>
  );
}

interface AccordionProps {
  items: { title: string; content: ReactNode }[];
  allowMultiple?: boolean;
}

export function Accordion({ items, allowMultiple = false }: AccordionProps) {
  const [openIndexes, setOpenIndexes] = useState<Set<number>>(new Set());

  const toggle = (index: number) => {
    setOpenIndexes(prev => {
      const next = new Set(allowMultiple ? prev : []);
      if (next.has(index)) next.delete(index);
      else next.add(index);
      return next;
    });
  };

  return (
    <div>
      {items.map((item, i) => (
        <AccordionItem key={i} title={item.title} isOpen={openIndexes.has(i)} onToggle={() => toggle(i)}>
          {item.content}
        </AccordionItem>
      ))}
    </div>
  );
}

Use Cases

  • FAQ sections
  • Settings panels

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.