Build a Accordion
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.
An accordion is a classic component-design exercise: simple state, but accessibility and the API design are where the signal is.
API & state
<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 → aSetof open ids. allowMultipleprop toggles the behavior — one prop, two modes.- Support controlled and uncontrolled (
defaultOpenvsopen/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: autoisn't animatable. Options: animategrid-template-rows: 0fr → 1fr, or measurescrollHeightand 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-controlson the button → the panel'sid; the panel hasrole="region"andaria-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
hiddenwhen 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.