Accordion

Muestra y oculta contenido expandible.

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}

Instalación

Copia y pega el siguiente código en tu proyecto.

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 };

Asegúrate de actualizar las rutas de importación según la estructura de tu proyecto.

Modo de uso

import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from '@/components/ui/accordion';
<Accordion type="single" defaultValue="item-1">
<AccordionItem value="item-1">
<AccordionTrigger>¿Es accesible?</AccordionTrigger>
<AccordionContent>Sí. Se adhiere al estándar de diseño WAI-ARIA.</AccordionContent>
</AccordionItem>
</Accordion>

Ejemplos

Single

El tipo single permite que solo un ítem esté abierto a la vez.

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

El tipo multiple permite que múltiples ítems estén abiertos simultáneamente.

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}

Sin valor por defecto

Puedes omitir defaultValue para que el acordeón inicie completamente cerrado.

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}

Referencia de API

Accordion

PropTipoDefaultDescripción
type"single" | "multiple""single"Controla si se puede abrir un solo ítem o varios simultáneamente.
defaultValuestringundefinedÍtem inicial abierto.
childrenReact.ReactNodeDeben ser AccordionItem.
classNamestringClases adicionales para el contenedor.

AccordionItem

PropTipoDefaultDescripción
valuestringID único del ítem. Obligatorio.
childrenReact.ReactNodeDebe incluir AccordionTrigger y AccordionContent.
isOpenbooleanControlado internoIndica si el ítem está abierto.
onToggle() => voidControlado internoFunción para abrir/cerrar.
classNamestringClases adicionales para el wrapper.

AccordionTrigger

PropTipoDefaultDescripción
childrenReact.ReactNodeTítulo o contenido del botón.
isOpenbooleanControlado internoEstado abierto/cerrado (rota el ícono).
onToggle() => voidControlado internoAcción al hacer click.
classNamestringClases adicionales del botón.

AccordionContent

PropTipoDefaultDescripción
childrenReact.ReactNodeContenido colapsable.
isOpenbooleanControlado internoControla apertura y animación.
classNamestringClases adicionales para el contenedor animado.