Docs/Components/Accordion

Accordion

A vertically stacked set of interactive headings that reveal or hide associated content.

Contenu de la section 1

Installation

Install the component Accordion in your project using the CLI.

Accordion.tsx

pnpm dlx behsseui@latest add Accordion

Install the component manually.

Create a ui folder at the root of the project, then a component folder inside it, and finally a Accordion.tsx file in that folder.

Copy and paste the following code into your project.

ui/components/Accordion.tsx

1"use client"
2
3import type { ReactNode } from "react"
4import { createContext, useContext, useState, useEffect } from "react"
5import { cva, type VariantProps } from "class-variance-authority"
6import { cn } from "@/lib/utils"
7
8const accordionVariants = cva(
9 "border rounded-lg overflow-hidden",
10 {
11 variants: {
12 variant: {
13 default: "border-border",
14 ghost: "border-transparent",
15 },
16 },
17 defaultVariants: {
18 variant: "default",
19 }
20 }
21)
22
23const accordionItemVariants = cva(
24 "border-b last:border-b-0",
25 {
26 variants: {
27 variant: {
28 default: "border-border",
29 ghost: "border-transparent",
30 },
31 },
32 defaultVariants: {
33 variant: "default",
34 }
35 }
36)
37
38const accordionTriggerVariants = cva(
39 "flex w-full items-center justify-between py-4 px-4 text-sm font-medium transition-all hover:underline cursor-pointer [&[data-state=open]>svg]:rotate-180",
40 {
41 variants: {
42 variant: {
43 default: "",
44 ghost: "",
45 },
46 },
47 defaultVariants: {
48 variant: "default",
49 }
50 }
51)
52
53const accordionContentVariants = cva(
54 "overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
55 {
56 variants: {
57 variant: {
58 default: "",
59 ghost: "",
60 },
61 },
62 defaultVariants: {
63 variant: "default",
64 }
65 }
66)
67
68type AccordionContextType = {
69 openItems: string[]
70 toggleItem: (value: string) => void
71 variant?: "default" | "ghost" | null
72 type?: "single" | "multiple"
73}
74
75const AccordionContext = createContext<AccordionContextType | undefined>(undefined)
76
77function useAccordion() {
78 const context = useContext(AccordionContext)
79 if (!context) {
80 throw new Error("Accordion components must be used within an Accordion")
81 }
82 return context
83}
84
85type AccordionItemContextType = {
86 value: string
87}
88
89const AccordionItemContext = createContext<AccordionItemContextType | undefined>(undefined)
90
91function useAccordionItem() {
92 const context = useContext(AccordionItemContext)
93 if (!context) {
94 throw new Error("AccordionTrigger and AccordionContent must be used within an AccordionItem")
95 }
96 return context
97}
98
99type AccordionProps = {
100 children: ReactNode
101 className?: string
102 type?: "single" | "multiple"
103 defaultValue?: string | string[]
104 value?: string | string[]
105 onValueChange?: (value: string | string[]) => void
106} & VariantProps<typeof accordionVariants>
107
108export function Accordion({
109 children,
110 className,
111 variant = "default",
112 type = "single",
113 defaultValue,
114 value,
115 onValueChange,
116}: AccordionProps) {
117 const [internalOpenItems, setInternalOpenItems] = useState<string[]>(() => {
118 if (defaultValue) {
119 return Array.isArray(defaultValue) ? defaultValue : [defaultValue]
120 }
121 return []
122 })
123
124 const isControlled = value !== undefined
125 const openItems = isControlled
126 ? Array.isArray(value)
127 ? value
128 : [value]
129 : internalOpenItems
130
131 const toggleItem = (itemValue: string) => {
132 let newOpenItems: string[]
133
134 if (type === "single") {
135 newOpenItems = openItems.includes(itemValue) ? [] : [itemValue]
136 } else {
137 newOpenItems = openItems.includes(itemValue)
138 ? openItems.filter((v) => v !== itemValue)
139 : [...openItems, itemValue]
140 }
141
142 if (!isControlled) {
143 setInternalOpenItems(newOpenItems)
144 }
145
146 if (onValueChange) {
147 onValueChange(type === "single" ? newOpenItems[0] || "" : newOpenItems)
148 }
149 }
150
151 return (
152 <AccordionContext.Provider value={{ openItems, toggleItem, variant, type }}>
153 <div className={cn(accordionVariants({ variant, className }))}>
154 {children}
155 </div>
156 </AccordionContext.Provider>
157 )
158}
159
160type AccordionItemProps = {
161 children: ReactNode
162 value: string
163 className?: string
164 defaultOpen?: boolean
165}
166
167export function AccordionItem({ children, value, className, defaultOpen }: AccordionItemProps) {
168 const { variant, openItems, toggleItem } = useAccordion()
169
170 useEffect(() => {
171 if (defaultOpen && !openItems.includes(value)) {
172 toggleItem(value)
173 }
174 }, []) // eslint-disable-line react-hooks/exhaustive-deps
175
176 return (
177 <AccordionItemContext.Provider value={{ value }}>
178 <div className={cn(accordionItemVariants({ variant, className }))}>
179 {children}
180 </div>
181 </AccordionItemContext.Provider>
182 )
183}
184
185type AccordionTriggerProps = {
186 children: ReactNode
187 className?: string
188}
189
190export function AccordionTrigger({ children, className }: AccordionTriggerProps) {
191 const { openItems, toggleItem, variant } = useAccordion()
192 const { value } = useAccordionItem()
193
194 const isOpen = openItems.includes(value)
195
196 return (
197 <button
198 type="button"
199 className={cn(accordionTriggerVariants({ variant, className }))}
200 data-state={isOpen ? "open" : "closed"}
201 onClick={() => toggleItem(value)}
202 >
203 {children}
204 <svg
205 xmlns="http://www.w3.org/2000/svg"
206 width="16"
207 height="16"
208 viewBox="0 0 24 24"
209 fill="none"
210 stroke="currentColor"
211 strokeWidth="2"
212 strokeLinecap="round"
213 strokeLinejoin="round"
214 className="shrink-0 transition-transform duration-200"
215 >
216 <path d="m6 9 6 6 6-6" />
217 </svg>
218 </button>
219 )
220}
221
222type AccordionContentProps = {
223 children: ReactNode
224 className?: string
225}
226
227export function AccordionContent({ children, className }: AccordionContentProps) {
228 const { openItems, variant } = useAccordion()
229 const { value } = useAccordionItem()
230
231 const isOpen = openItems.includes(value)
232
233 return (
234 <div
235 className={cn(accordionContentVariants({ variant, className }))}
236 data-state={isOpen ? "open" : "closed"}
237 >
238 {isOpen && (
239 <div className="px-4 pb-4 pt-0">
240 {children}
241 </div>
242 )}
243 </div>
244 )
245}
246

Usages

Different variants and use cases for the Accordion component.

Default

A single accordion that allows only one item open at a time.

Content for section 1

Default.tsx

<Accordion type="single" defaultValue="item-1">
  <AccordionItem value="item-1">
    <AccordionTrigger>Section 1</AccordionTrigger>
    <AccordionContent>Content for section 1</AccordionContent>
  </AccordionItem>
  <AccordionItem value="item-2">
    <AccordionTrigger>Section 2</AccordionTrigger>
    <AccordionContent>Content for section 2</AccordionContent>
  </AccordionItem>
</Accordion>

Multiple

An accordion that allows multiple items to be open simultaneously.

Multiple.tsx

<Accordion type="multiple">
  <AccordionItem value="item-1">
    <AccordionTrigger>Section 1</AccordionTrigger>
    <AccordionContent>Content for section 1</AccordionContent>
  </AccordionItem>
  <AccordionItem value="item-2">
    <AccordionTrigger>Section 2</AccordionTrigger>
    <AccordionContent>Content for section 2</AccordionContent>
  </AccordionItem>
</Accordion>

Ghost

An accordion without visible borders.

Ghost.tsx

<Accordion type="single" variant="ghost">
  <AccordionItem value="item-1">
    <AccordionTrigger>Section 1</AccordionTrigger>
    <AccordionContent>Content for section 1</AccordionContent>
  </AccordionItem>
  <AccordionItem value="item-2">
    <AccordionTrigger>Section 2</AccordionTrigger>
    <AccordionContent>Content for section 2</AccordionContent>
  </AccordionItem>
</Accordion>