react-collapsible/Examples/FAQ accordion
ExamplesPattern
FAQ accordion
Four questions, one open at a time. A single piece of state decides which panel is expanded; the chevron, the fade, and the collapse all react to it. The answers are real, too — they cover how the library animates, what it runs on, and where collapsed content goes.
How it's put together
- One
openId, exclusive by construction. State is the id of the open panel, ornull. Opening question B setsopenIdto B's id — A'sopenprop turns false in the same render, so A animates closed while B animates open. Clicking the open question setsnulland closes everything. - The chevron keys off
aria-expanded. Every trigger already carriesaria-controlsandaria-expandedfor the accessibility contract. Rotation styles against that attribute (group-aria-expanded:rotate-180) — no separate rotation state, nothing to drift out of sync. - Stable, prefixed ids. Each id (
faq-accordion-animation, …) is both the panel's DOM id and its trigger'saria-controlsvalue. Deriving ids from the data — never from an array index — keeps them stable, and prefixing them with the page slug keeps them unique across the site. - The content fades with the collapse.
open:opacity-100is a conditional class: it flips on the same deferred frame as the expansion, so the fade tracks the grid animation.duration-500matches the stylesheet's default collapse duration.
This page shows the finished pattern; Building an accordion walks through constructing it from a blank file.
TSXfaq-accordion.tsx
import { Collapsible } from '@westopp/react-collapsible' import { useState } from 'react' type Faq = { id: string question: string answer: string } const faqs: Faq[] = [ { id: 'faq-accordion-animation', question: 'How does the animation work without measuring content?', answer: 'Each panel is a CSS Grid container whose row track transitions between 0fr and 1fr. The browser resolves the fr unit against the content, so no JavaScript ever reads a height — the animation is pure CSS.', }, { id: 'faq-accordion-browser-support', question: 'Which browsers are supported?', answer: 'Chrome 117+, Edge 117+, Safari 17.4+, and Firefox 129+ — the versions that ship @starting-style and transition-behavior: allow-discrete. In older browsers content still opens and closes; it snaps instead of animating.', }, { id: 'faq-accordion-hide-modes', question: 'What happens to collapsed content?', answer: 'By default it is display: none — out of the document flow and out of the accessibility tree. Switch to hideMode="hide" to keep it in the DOM with visibility: hidden and aria-hidden, so form state and iframes survive the collapse.', }, { id: 'faq-accordion-controlled', question: 'Does the component manage its own open state?', answer: 'No — it is fully controlled. You pass open as a prop and toggle it however you like. This accordion is one useState holding the id of the open panel; setting it closes the previous panel and opens the next in the same render.', }, ] export const FaqAccordion = () => { const [openId, setOpenId] = useState<string | null>(null) return ( <div className='rounded-(--radius-callout) border border-(--color-line) bg-(--color-bg-elev) px-5 py-2'> {faqs.map((faq) => { const open = openId === faq.id return ( <div key={faq.id} className='border-b border-(--color-line-soft) last:border-b-0'> <button type='button' aria-controls={faq.id} aria-expanded={open} onClick={() => setOpenId(open ? null : faq.id)} className='group flex w-full cursor-pointer items-center justify-between gap-4 py-4 text-left text-[15px] font-medium text-(--color-ink)' > {faq.question} <svg width='16' height='16' viewBox='0 0 16 16' fill='none' aria-hidden='true' className='shrink-0 text-(--color-ink-mute) transition-transform duration-300 group-aria-expanded:rotate-180'> <path d='M4 6l4 4 4-4' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round' /> </svg> </button> <Collapsible id={faq.id} open={open} className='opacity-0 transition-opacity duration-500 open:opacity-100'> <p className='pb-4 text-[14px] leading-relaxed text-(--color-ink-soft)'>{faq.answer}</p> </Collapsible> </div> ) })} </div> ) }