--- title: Collapsible description: An interactive component that can be expanded or collapsed to reveal or hide content. --- ## Installation CLI Manual ```bash npx lightswind@latest add collapsible ``` Copy and paste the following code into your project. ```tsx import * as React from "react"; import { motion, AnimatePresence, HTMLMotionProps } from "framer-motion"; import { cn } from "@/components/lib/utils"; // Assuming you have a utility for combining class names // --- Context --- interface CollapsibleContextValue { open: boolean; onOpenChange: (open: boolean) => void; disabled?: boolean; } const CollapsibleContext = React.createContext< CollapsibleContextValue | undefined >(undefined); // --- Root Component --- interface CollapsibleProps extends React.HTMLAttributes { open?: boolean; defaultOpen?: boolean; disabled?: boolean; onOpenChange?: (open: boolean) => void; } const Collapsible = React.forwardRef( ( { children, open, defaultOpen = false, disabled = false, onOpenChange, className, ...props }, ref ) => { const [isOpen, setIsOpen] = React.useState(defaultOpen); const isControlled = open !== undefined; const currentOpen = isControlled ? open : isOpen; const handleOpenChange = React.useCallback( (value: boolean) => { if (disabled) return; if (!isControlled) { setIsOpen(value); } onOpenChange?.(value); }, [disabled, isControlled, onOpenChange] ); return (
{children}
); } ); Collapsible.displayName = "Collapsible"; // --- Trigger Component --- interface CollapsibleTriggerProps extends React.ButtonHTMLAttributes { asChild?: boolean; } const CollapsibleTrigger = React.forwardRef< HTMLButtonElement, CollapsibleTriggerProps >(({ className, children, asChild = false, ...props }, ref) => { const context = React.useContext(CollapsibleContext); if (!context) { throw new Error("CollapsibleTrigger must be used within a Collapsible"); } const { open, onOpenChange, disabled } = context; const handleClick = () => { onOpenChange(!open); }; if (asChild) { return ( <> {React.Children.map(children, (child) => { if (React.isValidElement(child)) { const element = child as React.ReactElement; return React.cloneElement(element, { ...element.props, ref, onClick: (e: React.MouseEvent) => { handleClick(); if (element.props.onClick) { element.props.onClick(e); } }, disabled: disabled || element.props.disabled, "data-state": open ? "open" : "closed", "data-disabled": disabled ? "" : undefined, "aria-expanded": open, }); } return child; })} ); } return ( ); }); CollapsibleTrigger.displayName = "CollapsibleTrigger"; // --- Content Component --- // Omit event handlers that conflict with Framer Motion's types type OmittedHTMLAttributes = Omit< React.HTMLAttributes, | "onAnimationStart" | "onAnimationEnd" | "onAnimationIteration" | "onTransitionEnd" | "onDrag" | "onDragEnd" | "onDragEnter" | "onDragExit" | "onDragLeave" | "onDragOver" | "onDragStart" | "onDrop" | "onMouseDown" | "onMouseEnter" | "onMouseLeave" | "onMouseMove" | "onMouseOut" | "onMouseOver" | "onMouseUp" | "onTouchCancel" | "onTouchEnd" | "onTouchMove" | "onTouchStart" | "onPointerDown" | "onPointerMove" | "onPointerUp" | "onPointerCancel" | "onPointerEnter" | "onPointerLeave" | "onPointerOver" | "onPointerOut" | "onGotPointerCapture" | "onLostPointerCapture" >; interface CollapsibleContentProps extends OmittedHTMLAttributes { forceMount?: boolean; } const CollapsibleContent = React.forwardRef< HTMLDivElement, CollapsibleContentProps >(({ className, children, forceMount, ...props }, ref) => { const context = React.useContext(CollapsibleContext); if (!context) { throw new Error("CollapsibleContent must be used within a Collapsible"); } const { open } = context; const contentRef = React.useRef(null); React.useImperativeHandle(ref, () => contentRef.current!); // Framer Motion variants for a professional, smooth animation const variants = { closed: { height: 0, opacity: 0, scale: 0.98, y: -10, transition: { height: { duration: 0.2 }, opacity: { duration: 0.15 }, scale: { duration: 0.2 }, y: { duration: 0.2 } } }, open: { height: "auto", opacity: 1, scale: 1, y: 0, transition: { height: { type: "spring", damping: 25, stiffness: 300 } as any, opacity: { duration: 0.2, delay: 0.05 }, scale: { type: "spring", damping: 20, stiffness: 200 } as any, y: { type: "spring", damping: 20, stiffness: 200 } as any } }, }; return ( {(open || forceMount) && ( )} > {/* This inner div is what we measure if we needed it, but height: auto handles it now */}
{children}
)}
); }); CollapsibleContent.displayName = "CollapsibleContent"; export { Collapsible, CollapsibleTrigger, CollapsibleContent }; ```
## Usage ```tsx import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/lightswind/collapsible" ``` ```tsx Toggle

Content that can be expanded or collapsed.

```