Accordion Patterns

Build collapsible accordion components using Radix UI primitives.

Overview#

Accordions organize content into expandable sections. This pattern covers:

  • Basic Radix Accordion component
  • FAQ-style accordions
  • Multiple open sections
  • Controlled accordions
  • Nested accordions

Prerequisites#

npm install @radix-ui/react-accordion lucide-react

Basic Accordion#

A customizable accordion using Radix UI.

1// components/ui/Accordion.tsx 2'use client' 3 4import * as AccordionPrimitive from '@radix-ui/react-accordion' 5import { ChevronDown } from 'lucide-react' 6import { forwardRef } from 'react' 7import { cn } from '@/lib/utils' 8 9export const Accordion = AccordionPrimitive.Root 10 11export const AccordionItem = forwardRef< 12 React.ElementRef<typeof AccordionPrimitive.Item>, 13 React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item> 14>(({ className, ...props }, ref) => ( 15 <AccordionPrimitive.Item 16 ref={ref} 17 className={cn('border-b', className)} 18 {...props} 19 /> 20)) 21AccordionItem.displayName = 'AccordionItem' 22 23export const AccordionTrigger = forwardRef< 24 React.ElementRef<typeof AccordionPrimitive.Trigger>, 25 React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger> 26>(({ className, children, ...props }, ref) => ( 27 <AccordionPrimitive.Header className="flex"> 28 <AccordionPrimitive.Trigger 29 ref={ref} 30 className={cn( 31 'flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline', 32 '[&[data-state=open]>svg]:rotate-180', 33 className 34 )} 35 {...props} 36 > 37 {children} 38 <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" /> 39 </AccordionPrimitive.Trigger> 40 </AccordionPrimitive.Header> 41)) 42AccordionTrigger.displayName = 'AccordionTrigger' 43 44export const AccordionContent = forwardRef< 45 React.ElementRef<typeof AccordionPrimitive.Content>, 46 React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content> 47>(({ className, children, ...props }, ref) => ( 48 <AccordionPrimitive.Content 49 ref={ref} 50 className={cn( 51 'overflow-hidden text-sm transition-all', 52 'data-[state=closed]:animate-accordion-up', 53 'data-[state=open]:animate-accordion-down', 54 className 55 )} 56 {...props} 57 > 58 <div className="pb-4 pt-0">{children}</div> 59 </AccordionPrimitive.Content> 60)) 61AccordionContent.displayName = 'AccordionContent'

Tailwind Animation Config#

Add these animations to your Tailwind config.

1// tailwind.config.js 2module.exports = { 3 theme: { 4 extend: { 5 keyframes: { 6 'accordion-down': { 7 from: { height: '0' }, 8 to: { height: 'var(--radix-accordion-content-height)' } 9 }, 10 'accordion-up': { 11 from: { height: 'var(--radix-accordion-content-height)' }, 12 to: { height: '0' } 13 } 14 }, 15 animation: { 16 'accordion-down': 'accordion-down 0.2s ease-out', 17 'accordion-up': 'accordion-up 0.2s ease-out' 18 } 19 } 20 } 21}

FAQ Accordion#

A single-open accordion for FAQ sections.

1// components/FAQ.tsx 2import { 3 Accordion, 4 AccordionContent, 5 AccordionItem, 6 AccordionTrigger 7} from '@/components/ui/Accordion' 8 9const faqs = [ 10 { 11 question: 'How do I get started?', 12 answer: 'Sign up for a free account and follow our getting started guide.' 13 }, 14 { 15 question: 'What payment methods do you accept?', 16 answer: 'We accept all major credit cards, PayPal, and bank transfers.' 17 }, 18 { 19 question: 'Can I cancel my subscription?', 20 answer: 'Yes, you can cancel anytime. Your account will remain active until the end of your billing period.' 21 }, 22 { 23 question: 'Do you offer refunds?', 24 answer: 'We offer a 30-day money-back guarantee for all paid plans.' 25 } 26] 27 28export function FAQ() { 29 return ( 30 <section className="py-16"> 31 <div className="mx-auto max-w-3xl"> 32 <h2 className="mb-8 text-center text-3xl font-bold"> 33 Frequently Asked Questions 34 </h2> 35 36 <Accordion type="single" collapsible className="w-full"> 37 {faqs.map((faq, index) => ( 38 <AccordionItem key={index} value={`item-${index}`}> 39 <AccordionTrigger>{faq.question}</AccordionTrigger> 40 <AccordionContent>{faq.answer}</AccordionContent> 41 </AccordionItem> 42 ))} 43 </Accordion> 44 </div> 45 </section> 46 ) 47}

Multiple Open Accordion#

Allow multiple sections to be open simultaneously.

1// components/SettingsAccordion.tsx 2import { 3 Accordion, 4 AccordionContent, 5 AccordionItem, 6 AccordionTrigger 7} from '@/components/ui/Accordion' 8import { User, Bell, Shield, CreditCard } from 'lucide-react' 9 10export function SettingsAccordion() { 11 return ( 12 <Accordion type="multiple" className="w-full" defaultValue={['profile']}> 13 <AccordionItem value="profile"> 14 <AccordionTrigger> 15 <div className="flex items-center gap-2"> 16 <User className="h-4 w-4" /> 17 Profile Settings 18 </div> 19 </AccordionTrigger> 20 <AccordionContent> 21 <div className="space-y-4"> 22 <div> 23 <label className="text-sm font-medium">Display Name</label> 24 <input 25 type="text" 26 className="mt-1 w-full rounded border px-3 py-2" 27 /> 28 </div> 29 <div> 30 <label className="text-sm font-medium">Bio</label> 31 <textarea className="mt-1 w-full rounded border px-3 py-2" /> 32 </div> 33 </div> 34 </AccordionContent> 35 </AccordionItem> 36 37 <AccordionItem value="notifications"> 38 <AccordionTrigger> 39 <div className="flex items-center gap-2"> 40 <Bell className="h-4 w-4" /> 41 Notifications 42 </div> 43 </AccordionTrigger> 44 <AccordionContent> 45 <div className="space-y-3"> 46 <label className="flex items-center gap-2"> 47 <input type="checkbox" defaultChecked /> 48 <span>Email notifications</span> 49 </label> 50 <label className="flex items-center gap-2"> 51 <input type="checkbox" defaultChecked /> 52 <span>Push notifications</span> 53 </label> 54 <label className="flex items-center gap-2"> 55 <input type="checkbox" /> 56 <span>SMS notifications</span> 57 </label> 58 </div> 59 </AccordionContent> 60 </AccordionItem> 61 62 <AccordionItem value="security"> 63 <AccordionTrigger> 64 <div className="flex items-center gap-2"> 65 <Shield className="h-4 w-4" /> 66 Security 67 </div> 68 </AccordionTrigger> 69 <AccordionContent> 70 <div className="space-y-4"> 71 <button className="rounded bg-blue-600 px-4 py-2 text-white"> 72 Change Password 73 </button> 74 <button className="rounded border px-4 py-2"> 75 Enable Two-Factor Auth 76 </button> 77 </div> 78 </AccordionContent> 79 </AccordionItem> 80 81 <AccordionItem value="billing"> 82 <AccordionTrigger> 83 <div className="flex items-center gap-2"> 84 <CreditCard className="h-4 w-4" /> 85 Billing 86 </div> 87 </AccordionTrigger> 88 <AccordionContent> 89 <div className="space-y-4"> 90 <div className="rounded bg-gray-50 p-4"> 91 <p className="font-medium">Current Plan: Pro</p> 92 <p className="text-sm text-gray-600">$29/month</p> 93 </div> 94 <button className="text-sm text-blue-600 hover:underline"> 95 View billing history 96 </button> 97 </div> 98 </AccordionContent> 99 </AccordionItem> 100 </Accordion> 101 ) 102}

Controlled Accordion#

Programmatically control which sections are open.

1// components/ControlledAccordion.tsx 2'use client' 3 4import { useState } from 'react' 5import { 6 Accordion, 7 AccordionContent, 8 AccordionItem, 9 AccordionTrigger 10} from '@/components/ui/Accordion' 11 12export function ControlledAccordion() { 13 const [openItems, setOpenItems] = useState<string[]>(['item-1']) 14 15 const expandAll = () => { 16 setOpenItems(['item-1', 'item-2', 'item-3']) 17 } 18 19 const collapseAll = () => { 20 setOpenItems([]) 21 } 22 23 return ( 24 <div> 25 <div className="mb-4 flex gap-2"> 26 <button 27 onClick={expandAll} 28 className="text-sm text-blue-600 hover:underline" 29 > 30 Expand All 31 </button> 32 <button 33 onClick={collapseAll} 34 className="text-sm text-blue-600 hover:underline" 35 > 36 Collapse All 37 </button> 38 </div> 39 40 <Accordion 41 type="multiple" 42 value={openItems} 43 onValueChange={setOpenItems} 44 > 45 <AccordionItem value="item-1"> 46 <AccordionTrigger>Section 1</AccordionTrigger> 47 <AccordionContent>Content for section 1</AccordionContent> 48 </AccordionItem> 49 <AccordionItem value="item-2"> 50 <AccordionTrigger>Section 2</AccordionTrigger> 51 <AccordionContent>Content for section 2</AccordionContent> 52 </AccordionItem> 53 <AccordionItem value="item-3"> 54 <AccordionTrigger>Section 3</AccordionTrigger> 55 <AccordionContent>Content for section 3</AccordionContent> 56 </AccordionItem> 57 </Accordion> 58 </div> 59 ) 60}

Nested Accordion#

Accordions within accordions for hierarchical content.

1// components/NestedAccordion.tsx 2import { 3 Accordion, 4 AccordionContent, 5 AccordionItem, 6 AccordionTrigger 7} from '@/components/ui/Accordion' 8 9interface Category { 10 name: string 11 subcategories: { 12 name: string 13 items: string[] 14 }[] 15} 16 17const categories: Category[] = [ 18 { 19 name: 'Electronics', 20 subcategories: [ 21 { name: 'Phones', items: ['iPhone', 'Samsung', 'Google Pixel'] }, 22 { name: 'Laptops', items: ['MacBook', 'ThinkPad', 'XPS'] } 23 ] 24 }, 25 { 26 name: 'Clothing', 27 subcategories: [ 28 { name: 'Men', items: ['Shirts', 'Pants', 'Shoes'] }, 29 { name: 'Women', items: ['Dresses', 'Tops', 'Accessories'] } 30 ] 31 } 32] 33 34export function NestedAccordion() { 35 return ( 36 <Accordion type="single" collapsible> 37 {categories.map((category, i) => ( 38 <AccordionItem key={i} value={`category-${i}`}> 39 <AccordionTrigger className="text-lg font-semibold"> 40 {category.name} 41 </AccordionTrigger> 42 <AccordionContent> 43 <Accordion type="single" collapsible className="ml-4"> 44 {category.subcategories.map((sub, j) => ( 45 <AccordionItem key={j} value={`sub-${i}-${j}`}> 46 <AccordionTrigger>{sub.name}</AccordionTrigger> 47 <AccordionContent> 48 <ul className="ml-4 list-disc space-y-1"> 49 {sub.items.map((item, k) => ( 50 <li key={k} className="text-gray-600"> 51 {item} 52 </li> 53 ))} 54 </ul> 55 </AccordionContent> 56 </AccordionItem> 57 ))} 58 </Accordion> 59 </AccordionContent> 60 </AccordionItem> 61 ))} 62 </Accordion> 63 ) 64}

Best Practices#

  1. Use semantic headings - Accordion triggers should use proper heading levels
  2. Animate smoothly - Use CSS transitions for height changes
  3. Default to closed - Don't overwhelm users with all content visible
  4. Keep content concise - Accordions work best with short, focused content
  5. Consider mobile - Ensure touch targets are large enough
  • Tabs - Alternative for content organization
  • Navigation - Collapsible navigation menus
  • Cards - Card-based content display