Collapsible

Componente interactivo para mostrar y ocultar contenido.

Prompt engineering is the practice of creating clear and effective instructions so an AI model understands exactly what you want. Common techniques include:

  • Few-shot: Providing examples to guide the model.
  • Chain-of-thought: Asking the model to reason step by step.
  • System prompts: Defining the assistant’s role and behavior.
  • Temperature: Adjusting creativity vs. precision.
You are an expert assistant specialized in...
collapsible-demo.tsx
1import {
2 Collapsible,
3 CollapsibleContent,
4 CollapsibleTrigger,
5} from '@/components/ui/collapsible';
6
7export function CollapsibleDemo() {
8 return (
9 <div className="space-y-3">
10 <Collapsible variant="bordered" defaultOpen>
11 <CollapsibleTrigger>
12 <span className="text-lg font-semibold">Prompt Engineering</span>
13 </CollapsibleTrigger>
14 <CollapsibleContent>
15 <div className="space-y-3 pt-3">
16 <p className="text-muted-foreground text-sm">
17 Prompt engineering is the practice of creating clear and effective instructions so an
18 AI model understands exactly what you want. Common techniques include:
19 </p>
20 <ul className="text-muted-foreground list-disc space-y-1 pl-5 text-sm">
21 <li>
22 <strong>Few-shot:</strong> Providing examples to guide the model.
23 </li>
24 <li>
25 <strong>Chain-of-thought:</strong> Asking the model to reason step by step.
26 </li>
27 <li>
28 <strong>System prompts:</strong> Defining the assistant’s role and behavior.
29 </li>
30 <li>
31 <strong>Temperature:</strong> Adjusting creativity vs. precision.
32 </li>
33 </ul>
34 <div className="pt-2">
35 <code className="bg-secondary rounded px-2 py-1 text-xs">
36 You are an expert assistant specialized in...
37 </code>
38 </div>
39 </div>
40 </CollapsibleContent>
41 </Collapsible>
42 <Collapsible variant="bordered">
43 <CollapsibleTrigger>
44 <span className="text-lg font-semibold">RAG (Retrieval-Augmented Generation)</span>
45 </CollapsibleTrigger>
46 <CollapsibleContent>
47 <div className="space-y-3 pt-3">
48 <p className="text-muted-foreground text-sm">
49 RAG combines a language model with an external knowledge base. The AI retrieves
50 information from your documents and then generates an answer based on those sources.
51 </p>
52 <ul className="text-muted-foreground list-disc space-y-1 pl-5 text-sm">
53 <li>Reduces hallucinations by grounding answers in real data.</li>
54 <li>Allows up-to-date information without retraining models.</li>
55 <li>Perfect for documentation, support assistants, and intelligent search.</li>
56 </ul>
57 </div>
58 </CollapsibleContent>
59 </Collapsible>
60 <Collapsible variant="bordered">
61 <CollapsibleTrigger>
62 <span className="text-lg font-semibold">AI SDKs & Model Providers</span>
63 </CollapsibleTrigger>
64 <CollapsibleContent>
65 <div className="space-y-3 pt-3">
66 <p className="text-muted-foreground text-sm">
67 AI SDKs make it easy to connect your application with large language models. They
68 simplify authentication, requests, streaming, and model selection.
69 </p>
70 <ul className="text-muted-foreground list-disc space-y-1 pl-5 text-sm">
71 <li>
72 <strong>Groq:</strong> Extremely fast inference, ideal for real-time apps.
73 </li>
74 <li>
75 <strong>OpenAI:</strong> Access to GPT models, embeddings, and multimodal features.
76 </li>
77 <li>
78 <strong>Anthropic:</strong> Known for Claude models with strong reasoning and
79 safety.
80 </li>
81 <li>
82 <strong>Vercel AI SDK:</strong> A friendly layer for building chat and AI features
83 in React and Next.js.
84 </li>
85 </ul>
86 <p className="text-muted-foreground pt-2 text-xs">
87 *Quick note:* These SDKs help developers focus on the product experience instead of
88 handling low-level API details.
89 </p>
90 </div>
91 </CollapsibleContent>
92 </Collapsible>
93 </div>
94 );
95}

Instalación

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

collapsible-source.tsx
'use client';
import { cva } from 'class-variance-authority';
import { ChevronDown } from 'lucide-react';
import { AnimatePresence, HTMLMotionProps, motion } from 'motion/react';
import * as React from 'react';
import { cn } from '../../../lib/cn';
const collapsibleVariants = cva('', {
variants: {
variant: {
default: '',
bordered: 'rounded-lg border border-border',
card: 'rounded-lg border border-border bg-card shadow-sm',
},
},
defaultVariants: {
variant: 'default',
},
});
const collapsibleTriggerVariants = cva(
[
'flex w-full items-center justify-between',
'transition-all duration-200',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
'disabled:pointer-events-none disabled:opacity-50',
].join(' '),
{
variants: {
variant: {
default: 'hover:opacity-80',
bordered: 'p-4',
card: 'p-4',
},
},
defaultVariants: {
variant: 'default',
},
},
);
const collapsibleContentVariants = cva('overflow-hidden', {
variants: {
variant: {
default: '',
bordered: 'px-4 pb-4',
card: 'px-4 pb-4',
},
},
defaultVariants: {
variant: 'default',
},
});
interface CollapsibleContextValue {
isOpen: boolean;
setIsOpen: (open: boolean) => void;
disabled?: boolean;
variant?: 'default' | 'bordered' | 'card';
}
interface CollapsibleProps extends React.HTMLAttributes<HTMLDivElement> {
defaultOpen?: boolean;
open?: boolean;
onOpenChange?: (open: boolean) => void;
disabled?: boolean;
variant?: 'default' | 'bordered' | 'card';
}
interface CollapsibleTriggerProps extends Omit<HTMLMotionProps<'button'>, 'children'> {
showChevron?: boolean;
chevronIcon?: React.ReactNode;
chevronPosition?: 'left' | 'right';
asChild?: boolean;
children?: React.ReactNode;
}
interface CollapsibleContentProps extends Omit<HTMLMotionProps<'div'>, 'children'> {
forceMount?: boolean;
children?: React.ReactNode;
}
const CollapsibleContext = React.createContext<CollapsibleContextValue | null>(null);
const useCollapsibleContext = (): CollapsibleContextValue => {
const context = React.useContext(CollapsibleContext);
if (!context) {
throw new Error('Collapsible components must be used within Collapsible');
}
return context;
};
const Collapsible = React.forwardRef<HTMLDivElement, CollapsibleProps>(
(
{
className,
defaultOpen = false,
open: controlledOpen,
onOpenChange,
disabled = false,
variant = 'default',
children,
...props
},
ref,
) => {
const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
const isControlled = controlledOpen !== undefined;
const isOpen = isControlled ? controlledOpen : internalOpen;
const setIsOpen = React.useCallback(
(open: boolean) => {
if (disabled) return;
if (!isControlled) {
setInternalOpen(open);
}
onOpenChange?.(open);
},
[disabled, isControlled, onOpenChange],
);
const contextValue = React.useMemo<CollapsibleContextValue>(
() => ({ isOpen, setIsOpen, disabled, variant }),
[isOpen, setIsOpen, disabled, variant],
);
return (
<CollapsibleContext.Provider value={contextValue}>
<div
ref={ref}
data-state={isOpen ? 'open' : 'closed'}
data-disabled={disabled ? '' : undefined}
className={cn(collapsibleVariants({ variant }), className)}
{...props}
>
{children}
</div>
</CollapsibleContext.Provider>
);
},
);
Collapsible.displayName = 'Collapsible';
const CollapsibleTrigger = React.forwardRef<HTMLButtonElement, CollapsibleTriggerProps>(
(
{
className,
showChevron = true,
chevronIcon,
chevronPosition = 'right',
asChild = false,
children,
onClick,
...props
},
ref,
) => {
const { isOpen, setIsOpen, disabled, variant } = useCollapsibleContext();
const handleClick = React.useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
if (!disabled) {
setIsOpen(!isOpen);
onClick?.(e);
}
},
[disabled, isOpen, setIsOpen, onClick],
);
const chevron = chevronIcon ?? <ChevronDown className="h-4 w-4 shrink-0" />;
const iconTransition = { type: 'spring', stiffness: 300, damping: 20 } as const;
if (asChild && React.isValidElement(children)) {
return React.cloneElement(children, {
onClick: handleClick,
'data-state': isOpen ? 'open' : 'closed',
'aria-expanded': isOpen,
disabled,
} as React.HTMLAttributes<HTMLElement>);
}
return (
<motion.button
ref={ref}
type="button"
onClick={handleClick}
disabled={disabled}
data-state={isOpen ? 'open' : 'closed'}
aria-expanded={isOpen}
whileHover={{ scale: 1.005, backgroundColor: 'rgba(0,0,0,0.02)' }}
whileTap={{ scale: 0.99 }}
className={cn(collapsibleTriggerVariants({ variant }), className)}
{...props}
>
{showChevron && chevronPosition === 'left' && (
<motion.span
aria-hidden="true"
animate={{ rotate: isOpen ? 90 : 0 }}
transition={iconTransition}
className="mr-2"
>
{chevron}
</motion.span>
)}
<span className="flex-1 text-left">{children}</span>
{showChevron && chevronPosition === 'right' && (
<motion.span
aria-hidden="true"
animate={{ rotate: isOpen ? 180 : 0 }}
transition={iconTransition}
className="ml-2"
>
{chevron}
</motion.span>
)}
</motion.button>
);
},
);
CollapsibleTrigger.displayName = 'CollapsibleTrigger';
const CollapsibleContent = React.forwardRef<HTMLDivElement, CollapsibleContentProps>(
({ className, forceMount = false, children, ...props }, ref) => {
const { isOpen, variant } = useCollapsibleContext();
return (
<AnimatePresence initial={false} mode="sync">
{(isOpen || forceMount) && (
<motion.div
ref={ref}
initial={{ height: 0, opacity: 0, filter: 'blur(10px)' }}
animate={{
height: 'auto',
opacity: 1,
filter: 'blur(0px)',
transition: {
height: { duration: 0.3, ease: [0.04, 0.62, 0.23, 0.98] },
opacity: { duration: 0.25, delay: 0.05 },
filter: { duration: 0.3 },
},
}}
exit={{
height: 0,
opacity: 0,
filter: 'blur(10px)',
transition: {
height: { duration: 0.25, ease: [0.04, 0.62, 0.23, 0.98] },
opacity: { duration: 0.15 }, // Desaparece rápido para no estorbar
filter: { duration: 0.2 },
},
}}
className={cn(collapsibleContentVariants({ variant }), className)}
{...props}
>
<motion.div
initial={{ y: -8, scale: 0.98 }}
animate={{
y: 0,
scale: 1,
transition: { duration: 0.3, ease: 'easeOut' },
}}
exit={{
y: -8,
scale: 0.98,
transition: { duration: 0.2 },
}}
>
{children}
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
},
);
CollapsibleContent.displayName = 'CollapsibleContent';
export {
Collapsible,
CollapsibleContent,
collapsibleContentVariants,
CollapsibleTrigger,
collapsibleTriggerVariants,
collapsibleVariants,
};
export type { CollapsibleContentProps, CollapsibleProps, CollapsibleTriggerProps };

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

Modo de uso

import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
<Collapsible>
<CollapsibleTrigger>¿Puedo usarlo en mi proyecto?</CollapsibleTrigger>
<CollapsibleContent>Sí. Es gratuito y de código abierto.</CollapsibleContent>
</Collapsible>

Variantes

Bordered

Collapsible con borde y esquinas redondeadas.

A bordered collapsible adds a visual container around the content with a border and rounded corners, making it stand out from the surrounding content.

collapsible-bordered-demo.tsx
1import {
2 Collapsible,
3 CollapsibleContent,
4 CollapsibleTrigger,
5} from '@/components/ui/collapsible';
6
7export function CollapsibleBorderedDemo() {
8 return (
9 <div className="w-full max-w-md space-y-4">
10 <Collapsible variant="bordered" defaultOpen>
11 <CollapsibleTrigger>What is a bordered collapsible?</CollapsibleTrigger>
12 <CollapsibleContent>
13 <p>
14 A bordered collapsible adds a visual container around the content with a border and
15 rounded corners, making it stand out from the surrounding content.
16 </p>
17 </CollapsibleContent>
18 </Collapsible>
19
20 <Collapsible variant="bordered">
21 <CollapsibleTrigger>When should I use it?</CollapsibleTrigger>
22 <CollapsibleContent>
23 <p>
24 Use bordered collapsibles when you want to group related content visually or when you
25 need clear separation between different collapsible sections.
26 </p>
27 </CollapsibleContent>
28 </Collapsible>
29 </div>
30 );
31}

Card

Collapsible con estilo de tarjeta incluyendo borde, fondo y sombra.

The card variant provides the most visual prominence with a background color, border, and subtle shadow. It's perfect for important content that needs to stand out.

collapsible-card-demo.tsx
1import {
2 Collapsible,
3 CollapsibleContent,
4 CollapsibleTrigger,
5} from '@/components/ui/collapsible';
6
7export function CollapsibleCardDemo() {
8 return (
9 <div className="w-full max-w-md space-y-4">
10 <Collapsible variant="card" defaultOpen>
11 <CollapsibleTrigger>What makes the card variant special?</CollapsibleTrigger>
12 <CollapsibleContent>
13 <p>
14 The card variant provides the most visual prominence with a background color, border,
15 and subtle shadow. It's perfect for important content that needs to stand out.
16 </p>
17 </CollapsibleContent>
18 </Collapsible>
19
20 <Collapsible variant="card">
21 <CollapsibleTrigger>Best practices</CollapsibleTrigger>
22 <CollapsibleContent>
23 <p>
24 Use card variant sparingly for emphasis. Too many card-styled elements can make your
25 interface feel cluttered. Reserve it for primary information or key features.
26 </p>
27 </CollapsibleContent>
28 </Collapsible>
29 </div>
30 );
31}

Deshabilitado

Collapsible en estado deshabilitado.

This collapsible started open but is now disabled. The content is visible but cannot be collapsed.

collapsible-disabled-demo.tsx
1import {
2 Collapsible,
3 CollapsibleContent,
4 CollapsibleTrigger,
5} from '@/components/ui/collapsible';
6
7export function CollapsibleDisabledDemo() {
8 return (
9 <div className="w-full max-w-md space-y-4">
10 <Collapsible variant="bordered" disabled>
11 <CollapsibleTrigger>Disabled Collapsible (Closed)</CollapsibleTrigger>
12 <CollapsibleContent>
13 <p>This content cannot be revealed because the collapsible is disabled.</p>
14 </CollapsibleContent>
15 </Collapsible>
16
17 <Collapsible variant="bordered" disabled defaultOpen>
18 <CollapsibleTrigger>Disabled Collapsible (Open)</CollapsibleTrigger>
19 <CollapsibleContent>
20 <p>
21 This collapsible started open but is now disabled. The content is visible but cannot be
22 collapsed.
23 </p>
24 </CollapsibleContent>
25 </Collapsible>
26 </div>
27 );
28}

Sin chevron

Collapsible sin icono indicador.

collapsible-no-chevron-demo.tsx
1import {
2 Collapsible,
3 CollapsibleContent,
4 CollapsibleTrigger,
5} from '@/components/ui/collapsible';
6
7export function CollapsibleNoChevronDemo() {
8 return (
9 <div className="w-full max-w-md space-y-4">
10 <Collapsible variant="bordered">
11 <CollapsibleTrigger showChevron={false}>Click me (No Chevron)</CollapsibleTrigger>
12 <CollapsibleContent>
13 <p>
14 This collapsible doesn't show a chevron icon. Useful when you want a cleaner look or
15 when the expandable nature is obvious from context.
16 </p>
17 </CollapsibleContent>
18 </Collapsible>
19
20 <Collapsible variant="card">
21 <CollapsibleTrigger showChevron={false} className="font-semibold">
22 Another Example
23 </CollapsibleTrigger>
24 <CollapsibleContent>
25 <p>
26 Without the chevron, the trigger can look like a simple heading or button, making it
27 more subtle in your design.
28 </p>
29 </CollapsibleContent>
30 </Collapsible>
31 </div>
32 );
33}

Con icono personalizado

Usa un icono personalizado en lugar del chevron predeterminado.

collapsible-custom-icon-demo.tsx
1import { Minus, Plus } from 'lucide-react';
2import {
3 Collapsible,
4 CollapsibleContent,
5 CollapsibleTrigger,
6} from '@/components/ui/collapsible';
7
8export function CollapsibleCustomIconDemo() {
9 return (
10 <div className="w-full max-w-md space-y-4">
11 <Collapsible variant="bordered">
12 <CollapsibleTrigger chevronIcon={<Plus className="h-4 w-4" />}>
13 Custom Plus Icon
14 </CollapsibleTrigger>
15 <CollapsibleContent>
16 <p>
17 This collapsible uses a plus icon instead of the default chevron. The icon rotates when
18 opened, creating a nice visual transition.
19 </p>
20 </CollapsibleContent>
21 </Collapsible>
22
23 <Collapsible variant="card">
24 <CollapsibleTrigger chevronIcon={<Minus className="h-4 w-4" />}>
25 Custom Minus Icon
26 </CollapsibleTrigger>
27 <CollapsibleContent>
28 <p>
29 You can use any icon you want! This example uses a minus icon to indicate collapsible
30 content.
31 </p>
32 </CollapsibleContent>
33 </Collapsible>
34 </div>
35 );
36}

Referencia de API

Collapsible

PropTipoDefaultDescripción
defaultOpenbooleanfalseEstado inicial abierto (modo no controlado).
openbooleanEstado abierto controlado.
onOpenChange(open: boolean) => voidCallback que se ejecuta cuando cambia el estado abierto.
disabledbooleanfalseDesactiva la interacción.
variant'default' | 'bordered' | 'card''default'Estilo visual del contenedor collapsible.
classNamestringClases adicionales.
childrenReact.ReactNodeContenido dentro del collapsible.

CollapsibleTrigger

PropTipoDefaultDescripción
showChevronbooleantrueMuestra el ícono de chevron.
chevronIconReact.ReactNodeÍcono personalizado para el chevron.
chevronPosition'left' | 'right''right'Posición del ícono.
asChildbooleanfalsePermite usar un elemento personalizado como trigger.
childrenReact.ReactNodeTexto o contenido del trigger.
onClick(event: React.MouseEvent) => voidManejador de click (combinado con la lógica de abrir/cerrar).
classNamestringClases adicionales.

CollapsibleContent

PropTipoDefaultDescripción
forceMountbooleanfalseFuerza que se renderice incluso cuando está cerrado (útil para SSR).
childrenReact.ReactNodeContenido a mostrar u ocultar.
classNamestringClases adicionales.