Back to Machine Coding
Machine Coding
easy
mid

How would you build an accordion component from scratch?

Render a list of header/panel pairs; track which panel(s) are open in state. Support single vs multi-expand via a prop, animate height, and make it accessible — button headers, aria-expanded, aria-controls, region roles, and keyboard support.

5 min read·~20 min to think through

An accordion is a classic component-design exercise: simple state, but accessibility and the API design are where the signal is.

API & state

jsx
<Accordion allowMultiple={false}>
  <Accordion.Item title="Section 1">content…</Accordion.Item>
  <Accordion.Item title="Section 2">content…</Accordion.Item>
</Accordion>
  • State: which item(s) are open. Single-expand → store one openId (or index). Multi-expand → a Set of open ids.
  • allowMultiple prop toggles the behavior — one prop, two modes.
  • Support controlled and uncontrolled (defaultOpen vs open/onChange).
  • Compound components (Accordion + Accordion.Item) share state via context — cleaner than a giant array-of-objects prop.

Rendering & animation

  • Each item: a header button + a collapsible panel.
  • Toggle open state on header click.
  • Animating height: height: auto isn't animatable. Options: animate grid-template-rows: 0fr → 1fr, or measure scrollHeight and animate to a pixel value, or use <details>/CSS. Keep the open state in React; let CSS do the transition.
  • Unmount vs hide panel content: hide (hidden) for fast toggling; unmount if content is heavy.

Accessibility — the real grading criteria

  • The header is a <button> (not a <div onClick>) — focusable, keyboard-activatable for free.
  • aria-expanded={isOpen} on the button.
  • aria-controls on the button → the panel's id; the panel has role="region" and aria-labelledby → the button's id.
  • Keyboard: Enter/Space toggles (free with <button>); optionally Up/Down arrows to move between headers, Home/End to first/last.
  • Panel hidden when collapsed so it's out of the a11y tree and tab order.

Edge cases

  • All collapsed / all expanded states.
  • Single-expand: opening one closes the other.
  • Deep-linking to an open section.
  • Dynamic items added/removed.

How to answer

"List of header/panel pairs, open state in the parent — a single id for single-expand or a Set for multi, toggled by an allowMultiple prop. I'd build it as compound components sharing state via context, and support controlled/uncontrolled. The accessibility is the core: <button> headers, aria-expanded, aria-controls/role=region, hidden panels, keyboard support. For animation I'd transition with CSS (grid-rows or measured height) since height: auto doesn't animate."

Follow-up questions

  • How do you support both single-expand and multi-expand cleanly?
  • How do you animate height when height:auto isn't animatable?
  • What ARIA attributes and keyboard interactions does an accordion need?
  • Why compound components over a config-array prop?

Common mistakes

  • Using a div with onClick instead of a button for the header.
  • Missing aria-expanded / aria-controls.
  • Trying to transition height:auto and getting no animation.
  • Not removing collapsed panels from the tab order (hidden).
  • Hardcoding single or multi expand instead of a prop.

Performance considerations

  • Hide panels with `hidden` for instant re-toggle, or unmount heavy content to save memory. CSS transitions keep animation off the JS thread. Memoize items if the list is large.

Edge cases

  • Single-expand: opening one must close the others.
  • All-collapsed and all-expanded states.
  • Heavy panel content — unmount vs hide.
  • Items added/removed dynamically.

Real-world examples

  • FAQ sections, settings panels, mobile nav menus, Radix/Headless UI Accordion.

Senior engineer discussion

Seniors design the API first (compound components, allowMultiple prop, controlled/uncontrolled), treat accessibility as the spec — button headers, aria-expanded/controls, region roles, hidden panels, keyboard nav — and know the height:auto animation trick. They distinguish hide-vs-unmount for panel content.

Related questions