# Collapsible

An interactive component that expands and collapses a panel.

```tsx
"use client"

import { useState } from "react"
import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
import { HStack, VStack } from "@/components/ui/stack"
import { color, leading, radius, space, text } from "@/theme/tokens.stylex"
import { ChevronsUpDown } from "lucide-react"

const styles = stylex.create({
  root: { width: "100%", maxWidth: "22rem" },
  title: { fontSize: text.sm, lineHeight: leading.sm, fontWeight: 600 },
  row: {
    borderRadius: radius.md,
    borderWidth: 1,
    borderStyle: "solid",
    paddingInline: space.s4,
    paddingBlock: space.s2,
    fontSize: text.sm,
    lineHeight: leading.sm,
  },
  muted: { color: color.mutedForeground },
  medium: { fontWeight: 500 },
  content: { paddingBlockStart: space.s2 },
})

export default function CollapsibleDemo() {
  const [open, setOpen] = useState(false)

  return (
    <Collapsible open={open} onOpenChange={setOpen} xstyle={styles.root}>
      <VStack gap={2}>
        <HStack gap={4} align="center" justify="between" paddingInline={4}>
          <h4 {...stylex.props(styles.title)}>Order #4189</h4>
          <CollapsibleTrigger
            render={<Button variant="ghost" size="icon" aria-label="Toggle details" />}
          >
            <ChevronsUpDown />
          </CollapsibleTrigger>
        </HStack>
        <HStack align="center" justify="between" xstyle={styles.row}>
          <span {...stylex.props(styles.muted)}>Status</span>
          <span {...stylex.props(styles.medium)}>Shipped</span>
        </HStack>
      </VStack>
      <CollapsibleContent xstyle={styles.content}>
        <VStack gap={2}>
          <VStack xstyle={styles.row}>
            <p {...stylex.props(styles.medium)}>Shipping address</p>
            <p {...stylex.props(styles.muted)}>100 Market St, San Francisco</p>
          </VStack>
          <VStack xstyle={styles.row}>
            <p {...stylex.props(styles.medium)}>Items</p>
            <p {...stylex.props(styles.muted)}>2x Studio Headphones</p>
          </VStack>
        </VStack>
      </CollapsibleContent>
    </Collapsible>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex clsx
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation), and paste `state-styles.ts` from the same
   page. The trigger reads its open state and the panel reads its transition
   state through it. The examples render the
   trigger as a [Button](https://carbon.style/docs/components/button).

3. Copy and paste the following code into your project.

   ```tsx title="components/ui/collapsible.tsx"
   "use client"

   import type { CSSProperties } from "react"
   import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"
   import { clsx } from "clsx"

   import { stateStyles } from "@/lib/state-styles"
   import { duration, ease } from "@/theme/tokens.stylex"

   const styles = stylex.create({
     trigger: {
       "--carbon-collapsible-open": 0,
     },
     triggerOpen: { "--carbon-collapsible-open": 1 },
     content: {
       overflow: "hidden",
       height: "var(--collapsible-panel-height)",
       transitionProperty: "height",
       transitionDuration: {
         default: duration.panel,
         "@media (prefers-reduced-motion: reduce)": "0s",
       },
       transitionTimingFunction: ease.spring,
     },
     contentHidden: { height: 0 },
   })

   const chevrons = stylex.create({
     chevron: {
       transform: "rotate(calc(180deg * var(--carbon-collapsible-open, 0)))",
       transitionProperty: "transform",
       transitionDuration: {
         default: duration.move,
         "@media (prefers-reduced-motion: reduce)": "0s",
       },
       transitionTimingFunction: ease.standard,
     },
     chevronInline: {
       transform: "rotate(calc(90deg * var(--carbon-collapsible-open, 0)))",
       transitionProperty: "transform",
       transitionDuration: {
         default: duration.move,
         "@media (prefers-reduced-motion: reduce)": "0s",
       },
       transitionTimingFunction: ease.standard,
     },
   })

   const collapsibleStyles = { chevron: chevrons.chevron, chevronInline: chevrons.chevronInline }

   type Overrides = { xstyle?: StyleXStyles; className?: string }

   type CollapsibleProps = Omit<CollapsiblePrimitive.Root.Props, "className"> & Overrides

   function Collapsible({ xstyle, className, ...props }: CollapsibleProps) {
     const sx = stylex.props(xstyle)
     return (
       <CollapsiblePrimitive.Root
         data-slot="collapsible"
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

   type CollapsibleTriggerProps = Omit<CollapsiblePrimitive.Trigger.Props, "className" | "style"> &
     Overrides & { style?: CSSProperties }

   function CollapsibleTrigger({ xstyle, style, className, ...props }: CollapsibleTriggerProps) {
     return (
       <CollapsiblePrimitive.Trigger
         data-slot="collapsible-trigger"
         {...props}
         {...stateStyles((state) => [styles.trigger, state.open && styles.triggerOpen, xstyle], {
           className,
           style,
         })}
       />
     )
   }

   type CollapsibleContentProps = Omit<CollapsiblePrimitive.Panel.Props, "className" | "style"> &
     Overrides & { style?: CSSProperties }

   function CollapsibleContent({
     xstyle,
     style,
     className,
     children,
     ...props
   }: CollapsibleContentProps) {
     const sx = stylex.props(xstyle)
     return (
       <CollapsiblePrimitive.Panel
         data-slot="collapsible-content"
         {...props}
         {...stateStyles(
           (state) => [
             styles.content,
             (state.transitionStatus === "starting" || state.transitionStatus === "ending") &&
               styles.contentHidden,
           ],
           {
             style,
           },
         )}
       >
         <div data-slot="collapsible-content-inner" {...sx} className={clsx(sx.className, className)}>
           {children}
         </div>
       </CollapsiblePrimitive.Panel>
     )
   }

   export { Collapsible, CollapsibleContent, CollapsibleTrigger, collapsibleStyles }
   ```

4. Update the import paths to match your project setup.

## Usage

```tsx
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
```

```tsx
<Collapsible>
  <CollapsibleTrigger>Can I use this in my project?</CollapsibleTrigger>
  <CollapsibleContent>
    Yes. Free to use for personal and commercial projects. No attribution required.
  </CollapsibleContent>
</Collapsible>
```

## Composition

```text
Collapsible
├── CollapsibleTrigger
└── CollapsibleContent
```

`CollapsibleTrigger` is unstyled. Pass `render={<Button />}` to make it a
button. `CollapsibleContent` renders the Base UI `Panel` around an inner
element that holds the content box. The panel animates its height over
`300ms` when it opens and closes.

> **Note**
>
> Put the space above or below the panel inside it, as padding on
> `CollapsibleContent`, not as a gap or margin around it. A gap outside stays at
> full size while the panel shrinks to nothing, then disappears when the panel
> unmounts, so the close ends with a jump.

## Controlled state

Use the `open` and `onOpenChange` props to control the state.

```tsx
import { useState } from "react"

export function Example() {
  const [open, setOpen] = useState(false)

  return (
    <Collapsible open={open} onOpenChange={setOpen}>
      <CollapsibleTrigger>Toggle</CollapsibleTrigger>
      <CollapsibleContent>Content</CollapsibleContent>
    </Collapsible>
  )
}
```

## Basic

Spread `collapsibleStyles.chevron` on an icon inside the trigger to turn it
with the panel.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
  collapsibleStyles,
} from "@/components/ui/collapsible"
import { VStack } from "@/components/ui/stack"
import { space } from "@/theme/tokens.stylex"
import { ChevronDown } from "lucide-react"

const styles = stylex.create({
  card: { width: "100%", maxWidth: "24rem" },
  trigger: { width: "100%", justifyContent: "space-between" },
  content: { paddingInline: space.s2_5, paddingBlockEnd: space.s2_5 },
})

export default function CollapsibleBasic() {
  return (
    <Card xstyle={styles.card}>
      <CardContent>
        <Collapsible>
          <CollapsibleTrigger render={<Button variant="ghost" xstyle={styles.trigger} />}>
            Product details
            <ChevronDown {...stylex.props(collapsibleStyles.chevron)} />
          </CollapsibleTrigger>
          <CollapsibleContent xstyle={styles.content}>
            <VStack gap={2} align="start">
              <p>This panel can be expanded or collapsed to reveal additional content.</p>
              <Button size="xs">Learn More</Button>
            </VStack>
          </CollapsibleContent>
        </Collapsible>
      </CardContent>
    </Card>
  )
}
```

## Settings panel

Use a trigger button to reveal additional settings.

```tsx
"use client"

import { useState } from "react"
import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
import { Input } from "@/components/ui/input"
import { HStack, VStack } from "@/components/ui/stack"
import { space } from "@/theme/tokens.stylex"
import { Maximize, Minimize } from "lucide-react"

const styles = stylex.create({
  card: { width: "100%", maxWidth: "20rem" },
  content: { paddingBlockStart: space.s2 },
})

export default function CollapsibleSettings() {
  const [open, setOpen] = useState(false)

  return (
    <Card size="sm" xstyle={styles.card}>
      <CardHeader>
        <CardTitle>Radius</CardTitle>
        <CardDescription>Set the corner radius of the element.</CardDescription>
      </CardHeader>
      <CardContent>
        <Collapsible open={open} onOpenChange={setOpen}>
          <HStack gap={2} align="start">
            <VStack grow>
              <HStack gap={2}>
                <Input aria-label="Top left radius" placeholder="0" defaultValue={0} />
                <Input aria-label="Top right radius" placeholder="0" defaultValue={0} />
              </HStack>
              <CollapsibleContent xstyle={styles.content}>
                <HStack gap={2}>
                  <Input aria-label="Bottom left radius" placeholder="0" defaultValue={0} />
                  <Input aria-label="Bottom right radius" placeholder="0" defaultValue={0} />
                </HStack>
              </CollapsibleContent>
            </VStack>
            <CollapsibleTrigger
              render={
                <Button
                  variant="outline"
                  size="icon"
                  aria-label={open ? "Fewer options" : "More options"}
                />
              }
            >
              {open ? <Minimize /> : <Maximize />}
            </CollapsibleTrigger>
          </HStack>
        </Collapsible>
      </CardContent>
    </Card>
  )
}
```

## File tree

Use nested collapsibles to build a file tree. `collapsibleStyles.chevronInline`
turns a right-pointing chevron down.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
  collapsibleStyles,
} from "@/components/ui/collapsible"
import { VStack } from "@/components/ui/stack"
import { color, space } from "@/theme/tokens.stylex"
import { ChevronRight, File, Folder } from "lucide-react"

const styles = stylex.create({
  card: { width: "100%", maxWidth: "16rem" },
  row: { width: "100%", justifyContent: "flex-start" },
  file: { color: color.foreground },
  children: { marginBlockStart: space.s1, marginInlineStart: space.s5 },
})

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

const fileTree: FileTreeItem[] = [
  {
    name: "components",
    items: [
      {
        name: "ui",
        items: [{ name: "button.tsx" }, { name: "card.tsx" }, { name: "input.tsx" }],
      },
      { name: "login-form.tsx" },
      { name: "register-form.tsx" },
    ],
  },
  {
    name: "lib",
    items: [{ name: "utils.ts" }, { name: "api.ts" }],
  },
  {
    name: "hooks",
    items: [{ name: "use-media-query.ts" }, { name: "use-debounce.ts" }],
  },
  { name: "app.tsx" },
  { name: "layout.tsx" },
  { name: "package.json" },
]

function TreeItem({ item }: { item: FileTreeItem }) {
  if (item.items) {
    return (
      <Collapsible>
        <CollapsibleTrigger render={<Button variant="ghost" size="sm" xstyle={styles.row} />}>
          <ChevronRight {...stylex.props(collapsibleStyles.chevronInline)} />
          <Folder />
          {item.name}
        </CollapsibleTrigger>
        <CollapsibleContent xstyle={styles.children}>
          <VStack gap={1}>
            {item.items.map((child) => (
              <TreeItem key={child.name} item={child} />
            ))}
          </VStack>
        </CollapsibleContent>
      </Collapsible>
    )
  }
  return (
    <Button variant="link" size="sm" xstyle={[styles.row, styles.file]}>
      <File />
      {item.name}
    </Button>
  )
}

export default function CollapsibleFileTree() {
  return (
    <Card size="sm" xstyle={styles.card}>
      <CardHeader>
        <CardTitle>Explorer</CardTitle>
      </CardHeader>
      <CardContent>
        <VStack gap={1}>
          {fileTree.map((item) => (
            <TreeItem key={item.name} item={item} />
          ))}
        </VStack>
      </CardContent>
    </Card>
  )
}
```

## API reference

Pass `render` to replace a part's element or compose it with another component.

Every part wraps the Base UI `Collapsible` part of the same name and accepts
its props. See the [Base UI reference](https://base-ui.com/react/components/collapsible)
for the full list. Each part also takes `xstyle` and `className`.
`CollapsibleTrigger` and `CollapsibleContent` take `style` as real inline CSS,
merged over the compiled styles.

### Collapsible

| Prop           | Type                               | Default |
| -------------- | ---------------------------------- | ------- |
| `open`         | `boolean`                          | —       |
| `defaultOpen`  | `boolean`                          | `false` |
| `onOpenChange` | `(open: boolean, details) => void` | —       |
| `disabled`     | `boolean`                          | `false` |

### CollapsibleContent

| Prop               | Type      | Default |
| ------------------ | --------- | ------- |
| `keepMounted`      | `boolean` | `false` |
| `hiddenUntilFound` | `boolean` | `false` |

`hiddenUntilFound` hides the closed panel with `hidden="until-found"` instead
of unmounting it, which lets the browser's find-in-page reach the text and open
the panel. It overrides `keepMounted`.

`CollapsibleContent` applies `xstyle` and `className` to the inner element and
`style` to the panel. The panel
animates its height between `0` and the measured content height, and a padded
box of height `0` still draws its padding.

### collapsibleStyles

Styles for an icon inside the trigger. Each reads the open state from the
trigger and has its own transition.

| Key             | What it is                                               |
| --------------- | -------------------------------------------------------- |
| `chevron`       | Turns a down-pointing chevron up when the panel opens    |
| `chevronInline` | Turns a right-pointing chevron down when the panel opens |

### Data attributes

| Attribute                                  | On                   | Effect                                       |
| ------------------------------------------ | -------------------- | -------------------------------------------- |
| `data-slot="collapsible-content"`          | the panel            | Targets the animated panel                   |
| `data-starting-style`, `data-ending-style` | the panel            | Base UI writes them while the panel animates |
| `data-open`, `data-disabled`               | root, trigger, panel | Base UI writes them                          |
| `data-panel-open`                          | the trigger          | Base UI writes it                            |

### CSS variables

| Variable                     | On          | Effect                                                                      |
| ---------------------------- | ----------- | --------------------------------------------------------------------------- |
| `--collapsible-panel-height` | the panel   | Base UI writes the measured height; the panel transitions to it             |
| `--carbon-collapsible-open`  | the trigger | `0` closed, `1` open. Inherited, so anything inside the trigger can read it |
