Collapsible

An interactive component which expands/collapses a panel.

Demo

Dependencies

lucide-reactclsxtailwind-merge

Props

This is a base primitive. It accepts standard HTML attributes plus className for styling via cn().

Source

"use client";

import { useState, useCallback } from "react";
import { cn } from "@/lib/utils";
import { ChevronDown } from "lucide-react";

export interface CollapsibleProps {
  defaultOpen?: boolean;
  open?: boolean;
  onOpenChange?: (open: boolean) => void;
  children?: React.ReactNode;
  className?: string;
}

export function Collapsible({
  defaultOpen = false,
  open: controlledOpen,
  onOpenChange,
  children,
  className,
}: CollapsibleProps) {
  const [internalOpen, setInternalOpen] = useState(defaultOpen);
  const isControlled = controlledOpen !== undefined;
  const open = isControlled ? controlledOpen : internalOpen;

  const handleToggle = useCallback(() => {
    const next = !open;
    if (!isControlled) setInternalOpen(next);
    onOpenChange?.(next);
  }, [open, isControlled, onOpenChange]);

  return (
    <CollapsibleContext.Provider value={{ open, onToggle: handleToggle }}>
      <div className={cn("space-y-1", className)}>{children}</div>
    </CollapsibleContext.Provider>
  );
}

import { createContext, useContext } from "react";

interface CollapsibleContextValue {
  open: boolean;
  onToggle: () => void;
}

const CollapsibleContext = createContext<CollapsibleContextValue>({
  open: false,
  onToggle: () => {},
});

export function CollapsibleTrigger({
  children,
  className,
  ...props
}: React.ButtonHTMLAttributes<HTMLButtonElement>) {
  const { onToggle } = useContext(CollapsibleContext);
  return (
    <button
      type="button"
      onClick={onToggle}
      className={cn(
        "flex items-center gap-2 text-sm font-medium text-gray-900 hover:text-gray-600",
        className,
      )}
      {...props}
    >
      <ChevronDown className="h-4 w-4 text-gray-500" />
      {children}
    </button>
  );
}

export function CollapsibleContent({
  children,
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) {
  const { open } = useContext(CollapsibleContext);
  if (!open) return null;

  return (
    <div className={cn("pl-6 text-sm text-gray-600", className)} {...props}>
      {children}
    </div>
  );
}

CLI

npx genui add collapsible