Siteplane UI

Collapsible

Show or hide a single section of content.

Markdown
"use client";

import { ChevronDownIcon } from "lucide-react";
import { Code } from "@/siteplane/components/code";
import {
  Collapsible,
  CollapsiblePanel,
  CollapsibleTrigger,
} from "@/siteplane/components/collapsible";

const recoveryKeys = ["4829-1735-6621", "9182-6407-5532", "3051-7924-9018"];

export default function Example() {
  return (
    <Collapsible className="w-full max-w-xs">
      <CollapsibleTrigger>
        Show recovery keys
        <ChevronDownIcon data-slot="collapsible-chevron" />
      </CollapsibleTrigger>
      <CollapsiblePanel>
        <ul className="flex flex-col items-start gap-1 py-2">
          {recoveryKeys.map((key) => (
            <li key={key}>
              <Code>{key}</Code>
            </li>
          ))}
        </ul>
      </CollapsiblePanel>
    </Collapsible>
  );
}

Installation

pnpm dlx shadcn@4.18.0 add @siteplane/collapsible

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

pnpm dlx shadcn@4.18.0 add @siteplane/button @siteplane/code @siteplane/frame @siteplane/separator

Usage

import {
  Collapsible,
  CollapsibleTrigger,
  CollapsiblePanel,
} from "@/siteplane/components/collapsible";

<Collapsible>
  <CollapsibleTrigger>Details</CollapsibleTrigger>
  <CollapsiblePanel>Content</CollapsiblePanel>
</Collapsible>;

Composition

Collapsible
├── CollapsibleTrigger
└── CollapsiblePanel / CollapsibleContent

Controlled state

Use open and onOpenChange when your application owns the open state. This trigger is an icon button, so it needs an accessible label.

Order #4189
Status
Shipped
"use client";

import { ChevronsUpDownIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/siteplane/components/button";
import {
  Collapsible,
  CollapsiblePanel,
  CollapsibleTrigger,
} from "@/siteplane/components/collapsible";
import {
  Frame,
  FrameDescription,
  FrameHeader,
  FramePanel,
  FrameTitle,
} from "@/siteplane/components/frame";
import { Separator } from "@/siteplane/components/separator";

export default function Example() {
  const [open, setOpen] = useState(false);
  return (
    <Collapsible className="w-full max-w-sm" onOpenChange={setOpen} open={open}>
      <Frame>
        <FrameHeader>
          <div className="flex items-center justify-between gap-4">
            <FrameTitle>Order #4189</FrameTitle>
            <CollapsibleTrigger
              render={
                <Button
                  aria-label="Toggle details"
                  shape="square"
                  variant="ghost"
                />
              }
            >
              <ChevronsUpDownIcon />
            </CollapsibleTrigger>
          </div>
        </FrameHeader>
        <FramePanel className="p-0">
          <div className="p-5">
            <FrameTitle>Status</FrameTitle>
            <FrameDescription>Shipped</FrameDescription>
          </div>
          <CollapsiblePanel>
            <Separator />
            <div className="p-5">
              <FrameTitle>Shipping address</FrameTitle>
              <FrameDescription>18 Harbor Street, Portland</FrameDescription>
            </div>
            <Separator />
            <div className="p-5">
              <FrameTitle>Items</FrameTitle>
              <FrameDescription>2 × Studio headphones</FrameDescription>
            </div>
          </CollapsiblePanel>
        </FramePanel>
      </Frame>
    </Collapsible>
  );
}

File tree

Nest collapsibles to reveal folders level by level. Pass render to use a smaller ghost button as the trigger. A ChevronRightIcon with data-slot="collapsible-chevron" and data-turn="quarter" points to the row's end and turns down while the folder is open; file rows keep the chevron's column empty, so names line up.

"use client";

import { ChevronRightIcon, FileIcon, FolderIcon } from "lucide-react";
import { Button } from "@/siteplane/components/button";
import {
  Collapsible,
  CollapsiblePanel,
  CollapsibleTrigger,
} from "@/siteplane/components/collapsible";

type FileTreeItem = { name: string; items?: FileTreeItem[] };

const files: FileTreeItem[] = [
  {
    name: "Website redesign",
    items: [
      {
        name: "Wireframes",
        items: [{ name: "Home.png" }, { name: "Pricing.png" }],
      },
      { name: "Project brief.pdf" },
      { name: "Timeline.xlsx" },
    ],
  },
  {
    name: "Mobile app",
    items: [{ name: "Requirements.docx" }, { name: "User research.pdf" }],
  },
  { name: "Team handbook.pdf" },
];

function renderItem(item: FileTreeItem) {
  if (item.items) {
    return (
      <Collapsible key={item.name}>
        <CollapsibleTrigger
          render={
            <Button align="start" size="sm" variant="ghost" width="full" />
          }
        >
          <ChevronRightIcon
            data-slot="collapsible-chevron"
            data-turn="quarter"
          />
          <FolderIcon />
          {item.name}
        </CollapsibleTrigger>
        <CollapsiblePanel className="ms-5">
          <div className="flex flex-col gap-1 pt-1">
            {item.items.map(renderItem)}
          </div>
        </CollapsiblePanel>
      </Collapsible>
    );
  }
  return (
    <Button
      align="start"
      key={item.name}
      size="sm"
      variant="ghost"
      width="full"
    >
      {/* The chevron's column, so file names line up with folder names. */}
      <span aria-hidden="true" className="w-(--sp-rw-button-icon) shrink-0" />
      <FileIcon />
      {item.name}
    </Button>
  );
}

export default function Example() {
  return (
    <div className="flex w-full max-w-64 flex-col gap-1">
      {files.map(renderItem)}
    </div>
  );
}

API reference

Component properties
PropTypeDefaultDescription
open / defaultOpenbooleanfalseControlled or initial visibility.
onOpenChange(open, eventDetails) => voidunsetVisibility callback.
disabledbooleanfalseDisables toggling.
CollapsibleTrigger.renderReactElement | render functiontext Button, full widthComposed trigger.
CollapsiblePanel.keepMountedbooleanfalseKeeps a closed panel in the DOM, hidden.

Collapsible, CollapsibleTrigger and CollapsiblePanel wrap Base UI Collapsible with no additional owned props. CollapsibleContent aliases Panel and CollapsiblePrimitive exports the namespace. Root passes props directly; styled Trigger/Panel accept string className. The panel animates its measured height.

CollapsibleTrigger doesn't add an indicator. Place a ChevronDownIcon with data-slot="collapsible-chevron" inside the trigger: Siteplane UI rotates it while the panel is open and skips the animation when reduced motion is requested. A ChevronRightIcon with the same slot and data-turn="quarter" makes a quarter turn instead, as in a tree.

Read the installed source.

Usage notes

Enter or Space activates the trigger. Keep its name meaningful in either state. Collapsible supplies disclosure behavior, not navigation or data loading.