Want to survive 12-hour coding sessions? Always keep snacks nearby and caffeine on standby. Bonus points for comfy socks and a chair that doesn't destroy your back.
Remember: comments are your friend. Future you will thank past you for writing clear notes.
1'use client';23import { Accordion } from '@/components/ui/accordion';45export function Default() {6 return (7 <Accordion type="single" className="w-full" defaultValue="item-1">8 <Accordion.Item value="item-1">9 <Accordion.Trigger value="item-1">Life Hacks for Coders</Accordion.Trigger>10 <Accordion.Content value="item-1" className="flex flex-col gap-4 text-balance">11 <p>12 Want to survive 12-hour coding sessions? Always keep snacks nearby and caffeine on13 standby. Bonus points for comfy socks and a chair that doesn't destroy your back.14 </p>15 <p>16 Remember: comments are your friend. Future you will thank past you for writing clear17 notes.18 </p>19 </Accordion.Content>20 </Accordion.Item>21 <Accordion.Item value="item-2">22 <Accordion.Trigger value="item-2">Debugging Secrets</Accordion.Trigger>23 <Accordion.Content value="item-2" className="flex flex-col gap-4 text-balance">24 <p>25 Debugging is basically detective work, but your suspects are lines of code. Breakpoints26 are your magnifying glass.27 </p>28 <p>29 Pro tip: if it compiles but doesn't work, stare at the screen, whisper "why won't you30 work?," then Google like your life depends on it.31 </p>32 </Accordion.Content>33 </Accordion.Item>34 <Accordion.Item value="item-3">35 <Accordion.Trigger value="item-3">Random Productivity Tips</Accordion.Trigger>36 <Accordion.Content value="item-3" className="flex flex-col gap-4 text-balance">37 <p>38 Sometimes the best way to get code done is to step away. Take a walk, pet your cat, or39 pretend to meditate.40 </p>41 <p>And remember: Ctrl+S is life. Save often, panic never.</p>42 </Accordion.Content>43 </Accordion.Item>44 </Accordion>45 );46}
Installation
Copy and paste the following code into your project.
'use client';import { AnimatePresence, motion, useReducedMotion } from 'motion/react';import * as React from 'react';import { cn } from '../lib/cn';// --- Animation variants (hoisted at module level to avoid recreation per render) ---const CHEVRON_VARIANTS = {open: { rotate: 180 },closed: { rotate: 0 },} as const;const CHEVRON_TRANSITION = {type: 'spring',stiffness: 200,damping: 15,} as const;const CHEVRON_STYLE = { willChange: 'transform' } as const;const CONTENT_HEIGHT_VARIANTS = {open: { height: 'auto' },closed: { height: 0 },} as const;const CONTENT_HEIGHT_TRANSITION = {duration: 0.3,ease: [0.04, 0.62, 0.23, 0.98] as [number, number, number, number],} as const;const CONTENT_FADE_VARIANTS = {open: { y: 0, opacity: 1, filter: 'blur(0px)' },closed: { y: -15, opacity: 0, filter: 'blur(6px)' },} as const;const CONTENT_FADE_TRANSITIONS = {enter: { duration: 0.35, ease: 'easeOut' },exit: { duration: 0.2, ease: 'easeIn' },} as const;const CONTENT_STYLE = { willChange: 'opacity, transform, filter' } as const;// --- Context ---type AccordionContextValue = {type: 'single' | 'multiple';openItems: string[];toggleItem: (value: string) => void;};const AccordionContext = React.createContext<AccordionContextValue | null>(null);const useAccordionContext = () => {const context = React.use(AccordionContext);if (!context) {throw new Error('Accordion components must be used within an Accordion');}return context;};// --- Components ---interface AccordionProps {type?: 'single' | 'multiple';defaultValue?: string;children?: React.ReactNode;className?: string;value?: string[];onValueChange?: (value: string[]) => void;}const AccordionRoot = ({type = 'single',defaultValue,children,className,value,onValueChange,ref,}: AccordionProps & { ref?: React.Ref<HTMLDivElement> }) => {const [uncontrolledValue, setUncontrolledValue] = React.useState<string[]>(defaultValue ? [defaultValue] : [],);const isControlled = value !== undefined;const openItems = isControlled ? value : uncontrolledValue;const toggleItem = React.useCallback((itemValue: string) => {const updater = (prev: string[]) => {if (type === 'single') {return prev.includes(itemValue) ? [] : [itemValue];}return prev.includes(itemValue)? prev.filter((v) => v !== itemValue): [...prev, itemValue];};if (!isControlled) {setUncontrolledValue((prev) => {const newValue = updater(prev);onValueChange?.(newValue);return newValue;});} else {onValueChange?.(updater(openItems));}},[type, isControlled, openItems, onValueChange],);return (<AccordionContext value={{ type, openItems, toggleItem }}><div ref={ref} className={cn('w-full space-y-2', className)}>{children}</div></AccordionContext>);};AccordionRoot.displayName = 'Accordion';interface AccordionItemProps extends React.HTMLAttributes<HTMLDivElement> {value: string;children?: React.ReactNode;}const AccordionItem = ({value,children,className,ref,...props}: AccordionItemProps & { ref?: React.Ref<HTMLDivElement> }) => {return (<divref={ref}className={cn('overflow-hidden border-b border-white/5 last:border-0', className)}{...props}>{children}</div>);};AccordionItem.displayName = 'AccordionItem';interface AccordionTriggerProps {children?: React.ReactNode;value: string;className?: string;}const AccordionTrigger = ({children,value,className,ref,}: AccordionTriggerProps & { ref?: React.Ref<HTMLButtonElement> }) => {const { openItems, toggleItem } = useAccordionContext();const isOpen = openItems.includes(value);const shouldReduceMotion = useReducedMotion();const triggerId = `accordion-trigger-${value.replace(/[^a-zA-Z0-9-]/gi, '')}`;const contentId = `accordion-content-${value.replace(/[^a-zA-Z0-9-]/gi, '')}`;return (<motion.buttonref={ref}id={triggerId}type="button"aria-controls={contentId}aria-expanded={isOpen}onClick={() => toggleItem(value)}whileTap={shouldReduceMotion ? undefined : { scale: 0.98 }}className={cn('group flex w-full items-center justify-between py-4 text-left text-sm font-medium transition-all hover:underline',className,)}>{children}<motion.svgxmlns="http://www.w3.org/2000/svg"width="24"height="24"viewBox="0 0 24 24"fill="none"stroke="currentColor"strokeWidth="2"strokeLinecap="round"strokeLinejoin="round"className="text-muted-foreground group-hover:text-primary h-4 w-4 shrink-0 transition-colors"variants={CHEVRON_VARIANTS}animate={shouldReduceMotion ? undefined : isOpen ? 'open' : 'closed'}transition={CHEVRON_TRANSITION}style={CHEVRON_STYLE}><path d="m6 9 6 6 6-6" /></motion.svg></motion.button>);};AccordionTrigger.displayName = 'AccordionTrigger';interface AccordionContentProps {children?: React.ReactNode;value: string;className?: string;}const AccordionContent = ({children,value,className,ref,}: AccordionContentProps & { ref?: React.Ref<HTMLDivElement> }) => {const { openItems } = useAccordionContext();const isOpen = openItems.includes(value);const triggerId = `accordion-trigger-${value.replace(/[^a-zA-Z0-9-]/gi, '')}`;const contentId = `accordion-content-${value.replace(/[^a-zA-Z0-9-]/gi, '')}`;return (<AnimatePresence initial={false} mode="wait">{isOpen && (<motion.divref={ref}id={contentId}role="region"aria-labelledby={triggerId}key="content"variants={CONTENT_HEIGHT_VARIANTS}initial="closed"animate="open"exit="closed"transition={CONTENT_HEIGHT_TRANSITION}className={cn('overflow-hidden text-sm', className)}><motion.divvariants={CONTENT_FADE_VARIANTS}initial="closed"animate="open"exit={{ ...CONTENT_FADE_VARIANTS.closed, transition: CONTENT_FADE_TRANSITIONS.exit }}transition={CONTENT_FADE_TRANSITIONS.enter}style={CONTENT_STYLE}className="text-muted-foreground pt-0 pb-4">{children}</motion.div></motion.div>)}</AnimatePresence>);};AccordionContent.displayName = 'AccordionContent';const Accordion = Object.assign(AccordionRoot, {Item: AccordionItem,Trigger: AccordionTrigger,Content: AccordionContent,});export { Accordion };export type { AccordionContentProps, AccordionItemProps, AccordionProps, AccordionTriggerProps };
Make sure to update the import paths to match your project structure.
Anatomy
import { Accordion } from '@/components/ui/accordion';
<Accordion type="single" defaultValue="item-1"><Accordion.Item value="item-1"><Accordion.Trigger>Is it accessible?</Accordion.Trigger><Accordion.Content>Yes. It adheres to the WAI-ARIA design pattern.</Accordion.Content></Accordion.Item></Accordion>
Examples
Single
The
single type allows only one item to be open at a time.Want to survive 12-hour coding sessions? Always keep snacks nearby and caffeine on standby. Bonus points for comfy socks and a chair that doesn't destroy your back.
Remember: comments are your friend. Future you will thank past you for writing clear notes.
1'use client';23import { Accordion } from '@/components/ui/accordion';45export function Default() {6 return (7 <Accordion type="single" className="w-full" defaultValue="item-1">8 <Accordion.Item value="item-1">9 <Accordion.Trigger value="item-1">Life Hacks for Coders</Accordion.Trigger>10 <Accordion.Content value="item-1" className="flex flex-col gap-4 text-balance">11 <p>12 Want to survive 12-hour coding sessions? Always keep snacks nearby and caffeine on13 standby. Bonus points for comfy socks and a chair that doesn't destroy your back.14 </p>15 <p>16 Remember: comments are your friend. Future you will thank past you for writing clear17 notes.18 </p>19 </Accordion.Content>20 </Accordion.Item>21 <Accordion.Item value="item-2">22 <Accordion.Trigger value="item-2">Debugging Secrets</Accordion.Trigger>23 <Accordion.Content value="item-2" className="flex flex-col gap-4 text-balance">24 <p>25 Debugging is basically detective work, but your suspects are lines of code. Breakpoints26 are your magnifying glass.27 </p>28 <p>29 Pro tip: if it compiles but doesn't work, stare at the screen, whisper "why won't you30 work?," then Google like your life depends on it.31 </p>32 </Accordion.Content>33 </Accordion.Item>34 <Accordion.Item value="item-3">35 <Accordion.Trigger value="item-3">Random Productivity Tips</Accordion.Trigger>36 <Accordion.Content value="item-3" className="flex flex-col gap-4 text-balance">37 <p>38 Sometimes the best way to get code done is to step away. Take a walk, pet your cat, or39 pretend to meditate.40 </p>41 <p>And remember: Ctrl+S is life. Save often, panic never.</p>42 </Accordion.Content>43 </Accordion.Item>44 </Accordion>45 );46}
Multiple
The
multiple type allows multiple items to be open simultaneously.1'use client';23import { Accordion } from '@/components/ui/accordion';45export function Multiple() {6 return (7 <Accordion type="multiple" className="w-full">8 <Accordion.Item value="item-1">9 <Accordion.Trigger value="item-1">Can I open multiple items?</Accordion.Trigger>10 <Accordion.Content value="item-1">11 Yes! When using type="multiple", you can have multiple accordion items open at the same12 time.13 </Accordion.Content>14 </Accordion.Item>15 <Accordion.Item value="item-2">16 <Accordion.Trigger value="item-2">How does it work?</Accordion.Trigger>17 <Accordion.Content value="item-2">18 Simply set the type prop to "multiple" and users can expand as many sections as they want19 simultaneously.20 </Accordion.Content>21 </Accordion.Item>22 <Accordion.Item value="item-3">23 <Accordion.Trigger value="item-3">Is this useful?</Accordion.Trigger>24 <Accordion.Content value="item-3">25 Absolutely! It's perfect for FAQ sections where users might want to compare multiple26 answers at once.27 </Accordion.Content>28 </Accordion.Item>29 </Accordion>30 );31}
Without default value
You can omit
defaultValue to have the accordion start completely collapsed.1'use client';23import { Accordion } from '@/components/ui/accordion';45export function Collapsed() {6 return (7 <Accordion type="single" className="w-full">8 <Accordion.Item value="item-1">9 <Accordion.Trigger value="item-1">Will it start closed?</Accordion.Trigger>10 <Accordion.Content value="item-1">11 Yes! When you don't provide a defaultValue prop, all items start in a collapsed state.12 </Accordion.Content>13 </Accordion.Item>14 <Accordion.Item value="item-2">15 <Accordion.Trigger value="item-2">Can users still open items?</Accordion.Trigger>16 <Accordion.Content value="item-2">17 Of course! Users can click any trigger to expand the content. It just starts fully18 collapsed.19 </Accordion.Content>20 </Accordion.Item>21 <Accordion.Item value="item-3">22 <Accordion.Trigger value="item-3">When is this useful?</Accordion.Trigger>23 <Accordion.Content value="item-3">24 This is great when you want users to actively choose what information they want to see,25 keeping the interface clean initially.26 </Accordion.Content>27 </Accordion.Item>28 </Accordion>29 );30}
API Reference
Accordion
| Prop | Type | Default | Description |
|---|---|---|---|
type | "single" | "multiple" | "single" | Controls whether only one item or multiple items can be open at the same time. |
defaultValue | string | undefined | The initially opened item. |
children | React.ReactNode | — | Should be AccordionItem components. |
className | string | — | Additional classes for the wrapper. |
AccordionItem
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | Unique identifier for the item. Required. |
children | React.ReactNode | — | Should include AccordionTrigger and AccordionContent. |
isOpen | boolean | Internal | Determines whether the item is open. |
onToggle | () => void | Internal | Function to toggle open/closed state. |
className | string | — | Additional classes for the wrapper. |
AccordionTrigger
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | The title or content of the button. |
isOpen | boolean | Internal | Whether the item is open (controls icon rotation). |
onToggle | () => void | Internal | Click handler for toggling. |
className | string | — | Additional classes for the trigger button. |
AccordionContent
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | Collapsible content. |
isOpen | boolean | Internal | Controls open state and animations. |
className | string | — | Additional classes for the animated content wrapper. |