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.
React accordions collapse related content into labeled toggles, and they show up in FAQ pages, filter sidebars, settings panels, and mobile navigation trees. The component is deceptively tricky to build well — you need to manage open state, animate height without janky reflow, preserve keyboard focus, and keep the markup accessible to screen readers.
Drivn's Accordion handles all four concerns through a small compound component: Accordion, Accordion.Item, Accordion.Trigger, and Accordion.Content. By default exactly one item is open at a time; pass multiple to allow several; pass defaultValue (a string or array of strings) to pre-open specific items on mount. Animation runs on a CSS grid trick — rows transition between 0fr and 1fr — so content height is read from the DOM with no JavaScript measurement and no ResizeObserver.
This page collects five Accordion patterns you will actually ship: a single-open FAQ, a multi-open settings panel, a default-open onboarding list, a controlled-state version for URL-driven panels, and a data-driven version that maps over an array. Every snippet is copy-paste and assumes you have installed Drivn via the CLI and imported Accordion from @/components/ui/accordion. For an engineering comparison against Radix-based alternatives, see Drivn vs shadcn/ui Accordion.
Simple FAQ accordion
The canonical accordion pattern is a single-open FAQ where clicking a question closes whichever one was open before. This is the default behavior — no props required. Each Accordion.Item gets a unique value string that identifies it in the internal state Set. Use short, question-style triggers and keep answers under two paragraphs; accordions discourage scanning, so long-form content is better as a separate page.
The accordion docs include a live preview of this exact pattern. Drop it into your marketing site's support page or pricing FAQ without any further setup.
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.