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.

accordion-demo.tsx
1import {
2 Accordion,
3 AccordionContent,
4 AccordionItem,
5 AccordionTrigger,
6} from '@/components/ui/accordion';
7
8export function AccordionDemo() {
9 return (
10 <Accordion type="single" className="w-full" defaultValue="item-1">
11 <AccordionItem value="item-1">
12 <AccordionTrigger>Life Hacks for Coders</AccordionTrigger>
13 <AccordionContent className="flex flex-col gap-4 text-balance">
14 <p>
15 Want to survive 12-hour coding sessions? Always keep snacks nearby and caffeine on
16 standby. Bonus points for comfy socks and a chair that doesn’t destroy your back.
17 </p>
18 <p>
19 Remember: comments are your friend. Future you will thank past you for writing clear
20 notes.
21 </p>
22 </AccordionContent>
23 </AccordionItem>
24 <AccordionItem value="item-2">
25 <AccordionTrigger>Debugging Secrets</AccordionTrigger>
26 <AccordionContent className="flex flex-col gap-4 text-balance">
27 <p>
28 Debugging is basically detective work, but your suspects are lines of code. Breakpoints
29 are your magnifying glass.
30 </p>
31 <p>
32 Pro tip: if it compiles but doesn’t work, stare at the screen, whisper “why won’t you
33 work?,” then Google like your life depends on it.
34 </p>
35 </AccordionContent>
36 </AccordionItem>
37 <AccordionItem value="item-3">
38 <AccordionTrigger>Random Productivity Tips</AccordionTrigger>
39 <AccordionContent className="flex flex-col gap-4 text-balance">
40 <p>
41 Sometimes the best way to get code done is to step away. Take a walk, pet your cat, or
42 pretend to meditate.
43 </p>
44 <p>And remember: Ctrl+S is life. Save often, panic never.</p>
45 </AccordionContent>
46 </AccordionItem>
47 </Accordion>
48 );
49}

Installation

Copy and paste the following code into your project.

accordion-source.tsx
'use client';
import { AnimatePresence, motion } from 'motion/react';
import * as React from 'react';
import { cn } from '../../../lib/cn';
interface AccordionProps {
type?: 'single' | 'multiple';
defaultValue?: string;
children?: React.ReactNode;
className?: string;
}
function Accordion({ type = 'single', defaultValue, children, className }: AccordionProps) {
const [openItems, setOpenItems] = React.useState<string[]>(defaultValue ? [defaultValue] : []);
const handleToggle = (value: string) => {
setOpenItems((prev) =>
type === 'single'
? prev.includes(value)
? []
: [value]
: prev.includes(value)
? prev.filter((item) => item !== value)
: [...prev, value],
);
};
return (
<div className={cn('w-full space-y-2', className)}>
{React.Children.map(children, (child) =>
React.isValidElement(child)
? React.cloneElement(child, {
isOpen: openItems.includes(
(child as React.ReactElement<AccordionItemProps>).props.value,
),
onToggle: () =>
handleToggle((child as React.ReactElement<AccordionItemProps>).props.value),
} as Partial<AccordionItemProps>)
: child,
)}
</div>
);
}
interface AccordionItemProps {
value: string;
children?: React.ReactNode;
isOpen?: boolean;
onToggle?: () => void;
className?: string;
}
function AccordionItem({ children, isOpen, onToggle, className }: AccordionItemProps) {
return (
<div className={cn('overflow-hidden border-b border-white/5 last:border-0', className)}>
{React.Children.map(children, (child) =>
React.isValidElement(child)
? React.cloneElement(child as React.ReactElement<AccordionItemProps>, {
isOpen,
onToggle,
})
: child,
)}
</div>
);
}
interface AccordionTriggerProps {
children?: React.ReactNode;
isOpen?: boolean;
onToggle?: () => void;
className?: string;
}
function AccordionTrigger({ children, isOpen, onToggle, className }: AccordionTriggerProps) {
return (
<motion.button
onClick={onToggle}
type="button"
// Eliminado el whileHover para que no se mueva al pasar el mouse
whileTap={{ 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', // Volvimos al hover clásico de 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"
animate={{
rotate: isOpen ? 180 : 0,
}}
transition={{
duration: 0.3,
type: 'spring',
stiffness: 200,
damping: 15,
}}
>
<path d="m6 9 6 6 6-6" />
</motion.svg>
</motion.button>
);
}
interface AccordionContentProps {
children?: React.ReactNode;
isOpen?: boolean;
className?: string;
}
function AccordionContent({ children, isOpen, className }: AccordionContentProps) {
return (
<AnimatePresence initial={false} mode="wait">
{isOpen && (
<motion.div
key="content"
initial={{ height: 0 }}
animate={{ height: 'auto' }}
exit={{ height: 0 }}
transition={{
height: {
duration: 0.3,
ease: [0.04, 0.62, 0.23, 0.98],
},
}}
className={cn('overflow-hidden text-sm', className)}
>
<motion.div
// Mantenemos la animación de blur y opacidad al abrirse
initial={{
y: -15,
opacity: 0,
filter: 'blur(6px)',
}}
animate={{
y: 0,
opacity: 1,
filter: 'blur(0px)',
}}
exit={{
y: -15,
opacity: 0,
filter: 'blur(6px)',
transition: { duration: 0.2, ease: 'easeIn' },
}}
transition={{
duration: 0.35,
ease: 'easeOut',
}}
className="text-muted-foreground pt-0 pb-4"
>
{children}
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };

Make sure to update the import paths to match your project structure.

Usage

import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion';
<Accordion type="single" defaultValue="item-1">
<AccordionItem value="item-1">
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent>Yes. It adheres to the WAI-ARIA design pattern.</AccordionContent>
</AccordionItem>
</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.

accordion-demo.tsx
1import {
2 Accordion,
3 AccordionContent,
4 AccordionItem,
5 AccordionTrigger,
6} from '@/components/ui/accordion';
7
8export function AccordionDemo() {
9 return (
10 <Accordion type="single" className="w-full" defaultValue="item-1">
11 <AccordionItem value="item-1">
12 <AccordionTrigger>Life Hacks for Coders</AccordionTrigger>
13 <AccordionContent className="flex flex-col gap-4 text-balance">
14 <p>
15 Want to survive 12-hour coding sessions? Always keep snacks nearby and caffeine on
16 standby. Bonus points for comfy socks and a chair that doesn’t destroy your back.
17 </p>
18 <p>
19 Remember: comments are your friend. Future you will thank past you for writing clear
20 notes.
21 </p>
22 </AccordionContent>
23 </AccordionItem>
24 <AccordionItem value="item-2">
25 <AccordionTrigger>Debugging Secrets</AccordionTrigger>
26 <AccordionContent className="flex flex-col gap-4 text-balance">
27 <p>
28 Debugging is basically detective work, but your suspects are lines of code. Breakpoints
29 are your magnifying glass.
30 </p>
31 <p>
32 Pro tip: if it compiles but doesn’t work, stare at the screen, whisper “why won’t you
33 work?,” then Google like your life depends on it.
34 </p>
35 </AccordionContent>
36 </AccordionItem>
37 <AccordionItem value="item-3">
38 <AccordionTrigger>Random Productivity Tips</AccordionTrigger>
39 <AccordionContent className="flex flex-col gap-4 text-balance">
40 <p>
41 Sometimes the best way to get code done is to step away. Take a walk, pet your cat, or
42 pretend to meditate.
43 </p>
44 <p>And remember: Ctrl+S is life. Save often, panic never.</p>
45 </AccordionContent>
46 </AccordionItem>
47 </Accordion>
48 );
49}

Multiple

The multiple type allows multiple items to be open simultaneously.

accordion-multiple-demo.tsx
1import {
2 Accordion,
3 AccordionContent,
4 AccordionItem,
5 AccordionTrigger,
6} from '@/components/ui/accordion';
7
8export function AccordionMultipleDemo() {
9 return (
10 <Accordion type="multiple" className="w-full">
11 <AccordionItem value="item-1">
12 <AccordionTrigger>Can I open multiple items?</AccordionTrigger>
13 <AccordionContent>
14 Yes! When using type="multiple", you can have multiple accordion items open at the same
15 time.
16 </AccordionContent>
17 </AccordionItem>
18 <AccordionItem value="item-2">
19 <AccordionTrigger>How does it work?</AccordionTrigger>
20 <AccordionContent>
21 Simply set the type prop to "multiple" and users can expand as many sections as they want
22 simultaneously.
23 </AccordionContent>
24 </AccordionItem>
25 <AccordionItem value="item-3">
26 <AccordionTrigger>Is this useful?</AccordionTrigger>
27 <AccordionContent>
28 Absolutely! It's perfect for FAQ sections where users might want to compare multiple
29 answers at once.
30 </AccordionContent>
31 </AccordionItem>
32 </Accordion>
33 );
34}

Without default value

You can omit defaultValue to have the accordion start completely collapsed.

accordion-collapsed-demo.tsx
1import {
2 Accordion,
3 AccordionContent,
4 AccordionItem,
5 AccordionTrigger,
6} from '@/components/ui/accordion';
7
8export function AccordionCollapsedDemo() {
9 return (
10 <Accordion type="single" className="w-full">
11 <AccordionItem value="item-1">
12 <AccordionTrigger>Will it start closed?</AccordionTrigger>
13 <AccordionContent>
14 Yes! When you don't provide a defaultValue prop, all items start in a collapsed state.
15 </AccordionContent>
16 </AccordionItem>
17 <AccordionItem value="item-2">
18 <AccordionTrigger>Can users still open items?</AccordionTrigger>
19 <AccordionContent>
20 Of course! Users can click any trigger to expand the content. It just starts fully
21 collapsed.
22 </AccordionContent>
23 </AccordionItem>
24 <AccordionItem value="item-3">
25 <AccordionTrigger>When is this useful?</AccordionTrigger>
26 <AccordionContent>
27 This is great when you want users to actively choose what information they want to see,
28 keeping the interface clean initially.
29 </AccordionContent>
30 </AccordionItem>
31 </Accordion>
32 );
33}

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.