Accordion

Display and hide expandable content.

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';
2
3import { Accordion } from '@/components/ui/accordion';
4
5export 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 on
13 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 clear
17 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. Breakpoints
26 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 you
30 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, or
39 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 (
<div
ref={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.button
ref={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.svg
xmlns="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.div
ref={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.div
variants={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';
2
3import { Accordion } from '@/components/ui/accordion';
4
5export 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 on
13 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 clear
17 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. Breakpoints
26 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 you
30 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, or
39 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';
2
3import { Accordion } from '@/components/ui/accordion';
4
5export 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 same
12 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 want
19 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 multiple
26 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';
2
3import { Accordion } from '@/components/ui/accordion';
4
5export 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 fully
18 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

PropTypeDefaultDescription
type"single" | "multiple""single"Controls whether only one item or multiple items can be open at the same time.
defaultValuestringundefinedThe initially opened item.
childrenReact.ReactNodeShould be AccordionItem components.
classNamestringAdditional classes for the wrapper.

AccordionItem

PropTypeDefaultDescription
valuestringUnique identifier for the item. Required.
childrenReact.ReactNodeShould include AccordionTrigger and AccordionContent.
isOpenbooleanInternalDetermines whether the item is open.
onToggle() => voidInternalFunction to toggle open/closed state.
classNamestringAdditional classes for the wrapper.

AccordionTrigger

PropTypeDefaultDescription
childrenReact.ReactNodeThe title or content of the button.
isOpenbooleanInternalWhether the item is open (controls icon rotation).
onToggle() => voidInternalClick handler for toggling.
classNamestringAdditional classes for the trigger button.

AccordionContent

PropTypeDefaultDescription
childrenReact.ReactNodeCollapsible content.
isOpenbooleanInternalControls open state and animations.
classNamestringAdditional classes for the animated content wrapper.