# Collapsible

Show or hide a single section of content.

Recovery keys disclosure

[Complete TSX file](/examples/collapsible/default.tsx)

```tsx
"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

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/collapsible
```

With npm: `npx shadcn@4.18.0 add @siteplane/collapsible`. [Registry JSON](/registry/collapsible.json) lists source files and dependencies.

For this page’s examples, also install:

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

## Usage

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

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

## Composition

```text
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 details with an icon trigger

[Complete TSX file](/examples/collapsible/controlled.tsx)

```tsx
"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.

File tree with nested folders

[Complete TSX file](/examples/collapsible/file-tree.tsx)

```tsx
"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

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| open / defaultOpen | boolean | false | Controlled or initial visibility. |
| onOpenChange | (open, eventDetails) => void | unset | Visibility callback. |
| disabled | boolean | false | Disables toggling. |
| CollapsibleTrigger.render | ReactElement \| render function | text Button, full width | Composed trigger. |
| CollapsiblePanel.keepMounted | boolean | false | Keeps a closed panel in the DOM, hidden. |
`Collapsible`, `CollapsibleTrigger` and `CollapsiblePanel` wrap [Base UI
Collapsible](https://base-ui.com/react/components/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](/source/collapsible).

## Usage notes

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