React Accordion Examples — FAQ, Multi-Open, Controlled
Copy-paste React Accordion examples — FAQ, multi-open panels, default-open onboarding, URL-synced controlled state, and nested menus — via Drivn dot notation.
An accordion looks like a stack of clickable headers, but shipping one that feels right means solving four problems at once: tracking which panels are open, animating height without a layout jolt, keeping keyboard focus where the user left it, and staying legible to screen readers. Those concerns turn up everywhere accordions do — FAQ pages, filter sidebars, settings forms, and collapsible mobile navigation.
Drivn's Accordion answers all four with a compact compound component: Accordion, Accordion.Item, Accordion.Trigger, and Accordion.Content. One item opens at a time by default; add multiple to allow several; pass defaultValue as a string or array of strings to pre-open items on mount. Open state lives in a plain Set inside the root, and the expand animation is a pure-CSS grid trick — each panel transitions grid-template-rows from 0fr to 1fr, so the browser interpolates the real content height with no JavaScript measurement and no ResizeObserver.
Below are five Accordion patterns you will genuinely reach for: a single-open FAQ, a multi-open settings panel, a default-open onboarding list, a URL-synced controlled version, and a data-driven map over an array. Each snippet is copy-paste and assumes you have installed Drivn through the CLI and imported Accordion from @/components/ui/accordion. For the engineering trade-offs against Radix-based alternatives, read Drivn vs shadcn/ui Accordion.
Simple FAQ accordion
The most common accordion is a single-open FAQ: click one question and whichever answer was showing collapses. That is Drivn's default, so this pattern needs no props at all. Every Accordion.Item carries a unique value string, which is the id the root stores in its internal Set to decide what is open. Write short, question-style triggers and keep each answer to a paragraph or two — accordions hide content behind a click, so anything long-form reads better as its own page than buried in a panel.
The accordion docs render this exact markup as a live preview. Drop it onto a pricing page, a support center, or a product FAQ and it works with no wiring beyond the import.
1 <Accordion> 2 <Accordion.Item value="refund"> 3 <Accordion.Trigger>What is your refund policy?</Accordion.Trigger> 4 <Accordion.Content> 5 Full refund within 14 days. No questions asked. 6 </Accordion.Content> 7 </Accordion.Item> 8 <Accordion.Item value="support"> 9 <Accordion.Trigger>How do I contact support?</Accordion.Trigger> 10 <Accordion.Content> 11 Email support@example.com — we reply within one business day. 12 </Accordion.Content> 13 </Accordion.Item> 14 </Accordion>
Multiple items open at once
Some interfaces call for many open panels simultaneously — a long settings form where each section expands independently, a filter sidebar where multiple filters stay active at once, or a documentation outline where users want two sections expanded for comparison. Pass multiple to the root to enable this, and optionally defaultValue as an array of values to pre-open a subset on mount.
Combined with the checkbox or switch components, the multi-open Accordion becomes a clean filter UI for product grids and dashboards — each filter group sits in its own Accordion.Item with the controls inside.
1 <Accordion multiple defaultValue={['display', 'notifications']}> 2 <Accordion.Item value="display"> 3 <Accordion.Trigger>Display</Accordion.Trigger> 4 <Accordion.Content> 5 Theme, font size, and density controls. 6 </Accordion.Content> 7 </Accordion.Item> 8 <Accordion.Item value="notifications"> 9 <Accordion.Trigger>Notifications</Accordion.Trigger> 10 <Accordion.Content> 11 Email, push, and in-app preferences. 12 </Accordion.Content> 13 </Accordion.Item> 14 <Accordion.Item value="privacy"> 15 <Accordion.Trigger>Privacy</Accordion.Trigger> 16 <Accordion.Content> 17 Data sharing and analytics. 18 </Accordion.Content> 19 </Accordion.Item> 20 </Accordion>
Default-open for onboarding
When new users land on a help center or getting-started page, opening the first item on mount gives them an immediate taste of the content and invites a click on the second. Pass defaultValue as a single string to open that item on first render; the rest remain collapsed until interacted with.
Avoid passing more than one default value in a single-open accordion — React's state reconciliation resolves it to the last item in the list, which can read as a bug to anyone debugging the layout. If you want several panels open at once, combine multiple with a defaultValue array as shown in the previous example.
1 <Accordion defaultValue="step-1"> 2 <Accordion.Item value="step-1"> 3 <Accordion.Trigger>1. Install Drivn</Accordion.Trigger> 4 <Accordion.Content> 5 Run npx drivn@latest create to scaffold a project. 6 </Accordion.Content> 7 </Accordion.Item> 8 <Accordion.Item value="step-2"> 9 <Accordion.Trigger>2. Add components</Accordion.Trigger> 10 <Accordion.Content> 11 npx drivn add button dialog tabs 12 </Accordion.Content> 13 </Accordion.Item> 14 <Accordion.Item value="step-3"> 15 <Accordion.Trigger>3. Ship</Accordion.Trigger> 16 <Accordion.Content> 17 Deploy to Vercel or any Node host. 18 </Accordion.Content> 19 </Accordion.Item> 20 </Accordion>
Controlled open state
When the open panel needs to sync with the URL (for example ?section=billing), a parent component, or a global store, the built-in state is not enough. The simplest approach is to re-mount the Accordion with a new key whenever the URL changes, so the internal state re-initializes from the fresh defaultValue. For a fully controlled version, edit accordion.tsx directly to accept open and onOpenChange props.
This pattern pairs well with React Hook Form when you want an Accordion to auto-expand on validation errors — bind the URL or the form's current error field as the controlled open value.
1 'use client' 2 import { useSearchParams, useRouter } from 'next/navigation' 3 import { Accordion } from '@/components/ui/accordion' 4 5 export function UrlSyncedAccordion() { 6 const params = useSearchParams() 7 const router = useRouter() 8 const open = params.get('section') ?? 'profile' 9 10 return ( 11 <Accordion key={open} defaultValue={open}> 12 <Accordion.Item value="profile"> 13 <Accordion.Trigger 14 onClick={() => router.push('?section=profile')} 15 > 16 Profile 17 </Accordion.Trigger> 18 <Accordion.Content>Name, email, avatar.</Accordion.Content> 19 </Accordion.Item> 20 <Accordion.Item value="billing"> 21 <Accordion.Trigger 22 onClick={() => router.push('?section=billing')} 23 > 24 Billing 25 </Accordion.Trigger> 26 <Accordion.Content>Card, invoices, plan.</Accordion.Content> 27 </Accordion.Item> 28 </Accordion> 29 ) 30 }
Render from a data array
Most real-world FAQ pages, settings panels, and help centers read their content from a CMS, a config file, or an API endpoint. The natural shape is an array of objects mapped to Accordion.Item children — each object becomes one row, the value is the object's stable id, and the trigger and content read from the object's fields.
This pattern scales from five hand-written FAQ entries to hundreds of dynamically loaded sections without changing the surrounding markup. For long lists where only a handful of items should be visible at a time, pair it with the scroll-area component to cap the panel height. For search-driven FAQs, filter the array before mapping — the Accordion handles the re-render cleanly because each item's value acts as a React key.
1 const faqs = [ 2 { 3 id: 'refund', 4 question: 'What is your refund policy?', 5 answer: 'Full refund within 14 days. No questions asked.', 6 }, 7 { 8 id: 'support', 9 question: 'How do I contact support?', 10 answer: 'Email support@example.com — we reply within a day.', 11 }, 12 { 13 id: 'plan', 14 question: 'Can I change my plan later?', 15 answer: 'Yes, upgrade or downgrade any time from settings.', 16 }, 17 ] 18 19 <Accordion> 20 {faqs.map((faq) => ( 21 <Accordion.Item key={faq.id} value={faq.id}> 22 <Accordion.Trigger>{faq.question}</Accordion.Trigger> 23 <Accordion.Content>{faq.answer}</Accordion.Content> 24 </Accordion.Item> 25 ))} 26 </Accordion>
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
Frequently asked questions
Yes, but it requires editing the component source. The default state lives in a local Set inside the root. Add open and onOpenChange props to the component in src/components/ui/accordion.tsx, then replace the internal useState with a controlled fallback. Because the source sits in your repo after install, this is a one-time modification that you own forever — no upstream version bumps will overwrite it.
Yes. The CSS grid animation reads row height from the DOM, so images, iframes, and dynamically loaded content transition smoothly the moment they finish layout. The animation uses grid-template-rows transitioning from 0fr to 1fr, which interpolates fractional row heights without any JavaScript or ResizeObserver. Content of any size animates correctly, even when it loads after the open transition starts.
Drivn's Accordion triggers are native <button> elements with aria-expanded attributes, and the panel is wrapped in a role="region" container. Screen readers announce "collapsed" and "expanded" states and the region role correctly. Make sure your trigger content is meaningful text — avoid icon-only triggers that screen readers cannot narrate unless you add an aria-label fallback describing the section.
Yes. The ChevronDown icon is imported from lucide-react at the top of the component file. Swap it for any other Lucide icon (Plus, ChevronRight, etc.) or replace it with an inline SVG. Because the component lives in your repo, the edit is one line and the behavior updates immediately. The rotate-on-open class lives in the styles.icon string next to the other Tailwind classes.

