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-reactBasic 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#
- Use semantic headings - Accordion triggers should use proper heading levels
- Animate smoothly - Use CSS transitions for height changes
- Default to closed - Don't overwhelm users with all content visible
- Keep content concise - Accordions work best with short, focused content
- Consider mobile - Ensure touch targets are large enough
Related Patterns#
- Tabs - Alternative for content organization
- Navigation - Collapsible navigation menus
- Cards - Card-based content display