For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Collapsible

The Collapsible component from this design system, built on Base UI.

Installation

$ pnpm dlx shadcn@latest add @cobalt/collapsible

Options

  • CollapsiblePeek keeps its content in view while the collapsible is closed, clipped to height (16rem by default), and shows all of it when open. Use it instead of CollapsibleContent for a long block such as code, where the first lines are worth showing. It must sit inside Collapsible, which now carries the group/collapsible class it reads.

Source

"use client";

import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible";
import { cn } from "cn";
import * as React from "react";

function Collapsible({ className, ...props }: CollapsiblePrimitive.Root.Props) {
  return (
    <CollapsiblePrimitive.Root
      data-slot="collapsible"
      className={(state) =>
        cn(
          "group/collapsible",
          typeof className === "function" ? className(state) : className
        )
      }
      {...props}
    />
  );
}

function CollapsibleTrigger({
  className,
  ...props
}: CollapsiblePrimitive.Trigger.Props) {
  return (
    <CollapsiblePrimitive.Trigger
      data-slot="collapsible-trigger"
      className={(state) =>
        cn(
          "cursor-pointer data-disabled:cursor-default",
          typeof className === "function" ? className(state) : className
        )
      }
      {...props}
    />
  );
}

function CollapsibleContent({
  className,
  ...props
}: CollapsiblePrimitive.Panel.Props) {
  return (
    <CollapsiblePrimitive.Panel
      data-slot="collapsible-content"
      className={(state) =>
        cn(
          "h-(--collapsible-panel-height) overflow-clip transition-[height,opacity,translate,filter] duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] data-ending-style:duration-200 data-starting-style:h-0 data-starting-style:opacity-0 data-ending-style:h-0 data-ending-style:opacity-0 motion-safe:data-starting-style:-translate-y-1 motion-safe:data-starting-style:blur-[4px] motion-safe:data-ending-style:-translate-y-1 motion-safe:data-ending-style:blur-[4px] motion-reduce:transition-none",
          typeof className === "function" ? className(state) : className
        )
      }
      {...props}
    />
  );
}

// Content that stays in view while the collapsible is closed, clipped to
// `height`, and shows in full once it is open. Use it instead of
// CollapsibleContent for a long block such as code, where the first lines
// are worth showing. It must sit inside Collapsible.
// The height animates with the same timing as CollapsibleContent. CSS cannot
// transition to `auto`, so the full content height is measured and kept in
// --collapsible-peek-full. Until it is measured (first paint) the closed state
// falls back to a plain max-height clamp.
function CollapsiblePeek({
  className,
  height = "16rem",
  style,
  ref,
  ...props
}: React.ComponentProps<"div"> & { height?: string }) {
  const innerRef = React.useRef<HTMLDivElement | null>(null);
  const [full, setFull] = React.useState<number | null>(null);

  React.useLayoutEffect(() => {
    const el = innerRef.current;
    if (!el) {
      return;
    }
    const measure = () => {
      setFull(el.scrollHeight + (el.offsetHeight - el.clientHeight));
    };
    measure();
    // The box has a fixed height, so observe its children for size changes.
    const observer = new ResizeObserver(measure);
    const observeChildren = () => {
      observer.disconnect();
      for (const child of el.children) {
        observer.observe(child);
      }
    };
    observeChildren();
    const mutations = new MutationObserver(() => {
      observeChildren();
      measure();
    });
    mutations.observe(el, { childList: true });
    return () => {
      observer.disconnect();
      mutations.disconnect();
    };
  }, []);

  return (
    <div
      ref={(node) => {
        innerRef.current = node;
        if (typeof ref === "function") {
          ref(node);
        } else if (ref) {
          ref.current = node;
        }
      }}
      data-slot="collapsible-peek"
      data-measured={full === null ? undefined : ""}
      style={
        {
          "--collapsible-peek": height,
          ...(full === null
            ? null
            : { "--collapsible-peek-full": `${full}px` }),
          ...style,
        } as React.CSSProperties
      }
      className={cn(
        "relative overflow-hidden transition-[height] duration-250 ease-[cubic-bezier(0.22,1,0.36,1)] group-data-closed/collapsible:max-h-(--collapsible-peek) group-data-closed/collapsible:duration-200 data-measured:h-(--collapsible-peek-full) data-measured:max-h-none data-measured:group-data-closed/collapsible:h-[min(var(--collapsible-peek),var(--collapsible-peek-full))] motion-reduce:transition-none",
        className
      )}
      {...props}
    />
  );
}

export { Collapsible, CollapsibleTrigger, CollapsibleContent, CollapsiblePeek };