# Collapsible (/ui/components/react/collapsible)



<Playground component="collapsible" />

## Usage [#usage]

```tsx
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@appica/ui-react/collapsible'
```

```tsx
<Collapsible>
  <CollapsibleTrigger>What's included in the free plan?</CollapsibleTrigger>
  <CollapsibleContent>Up to three projects, 1 GB of storage, and community support.</CollapsibleContent>
</Collapsible>
```

`Collapsible` is a single show/hide disclosure — one trigger controlling one panel. Reach for it for a "read more", an inline details section, or any one-off toggle; for a set of mutually-aware sections use [`Accordion`](/ui/components/react/accordion) instead. Compose it from three parts, all under `@appica/ui-react/collapsible`:

* **`Collapsible`** — the root. Owns the open state (`open`/`defaultOpen`) and can be `disabled`.
* **`CollapsibleTrigger`** — the button that toggles the panel. It exposes `data-panel-open` and `data-disabled` for styling.
* **`CollapsibleContent`** — the panel, which animates its height open and closed. It publishes the `--collapsible-panel-height` CSS variable that powers that animation.

The parts ship almost unstyled so the collapsible inherits your layout — the examples below add the card, chevron, and spacing.

## Examples [#examples]

### Default [#default]

A trigger and a panel. The panel animates its height open and closed, and the leading chevron rotates a quarter-turn because the trigger carries `data-panel-open` while open. Pass `defaultOpen` to start expanded, as here.

```tsx
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@appica/ui-react/collapsible'
import { ChevronRight } from '@appica/icons-react'

export default function CollapsibleDefault() {
  return (
    <Collapsible defaultOpen className="w-full max-w-100">
      <CollapsibleTrigger className="group text-foreground-intense inline-flex items-start gap-2 text-start font-medium">
        <ChevronRight className="mt-0.75 size-4.5 shrink-0 stroke-2 transition-transform duration-200 group-data-panel-open:rotate-90 motion-reduce:transition-none" />
        What's included in the free plan?
      </CollapsibleTrigger>
      <CollapsibleContent>
        <p className="ps-6.5 pt-2.5 text-sm">
          The free plan includes up to three projects, 1&nbsp;GB of storage, and community support — everything you need
          to evaluate the product before upgrading.
        </p>
      </CollapsibleContent>
    </Collapsible>
  )
}
```

### File tree [#file-tree]

Nest a `Collapsible` inside another `Collapsible`'s content to build an expandable tree. Each folder is its own disclosure; files are plain rows. A recursive render keeps any depth tidy.

```tsx
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@appica/ui-react/collapsible'
import { ChevronRight, Folder, FolderOpen, File } from '@appica/icons-react'

type TreeNode = { name: string; children?: TreeNode[] }

const tree: TreeNode[] = [
  {
    name: 'src',
    children: [
      {
        name: 'app',
        children: [{ name: 'layout.tsx' }, { name: 'page.tsx' }, { name: 'globals.css' }],
      },
      {
        name: 'components',
        children: [{ name: 'site-header.tsx' }, { name: 'site-footer.tsx' }],
      },
      { name: 'lib', children: [{ name: 'utils.ts' }] },
    ],
  },
  {
    name: 'public',
    children: [{ name: 'favicon.ico' }, { name: 'logo.svg' }],
  },
  { name: 'package.json' },
  { name: 'README.md' },
]

function TreeItem({ node, defaultOpen }: { node: TreeNode; defaultOpen?: boolean }) {
  if (!node.children) {
    return (
      <span className="text-foreground-muted flex items-center gap-1.5 py-1 ps-5.5">
        <File className="size-4 shrink-0" />
        {node.name}
      </span>
    )
  }

  return (
    <Collapsible defaultOpen={defaultOpen}>
      <CollapsibleTrigger className="group inline-flex items-center gap-1.5 py-1 font-medium">
        <ChevronRight className="size-4 shrink-0 stroke-[1.75] transition-transform duration-200 group-data-panel-open:rotate-90 motion-reduce:transition-none" />
        <Folder className="text-foreground-muted size-4 shrink-0 stroke-[1.75] group-data-panel-open:hidden" />
        <FolderOpen className="text-foreground-muted hidden size-4 shrink-0 stroke-[1.75] group-data-panel-open:block" />
        {node.name}
      </CollapsibleTrigger>
      <CollapsibleContent>
        <div className="ms-2 flex flex-col border-s ps-2">
          {node.children.map((child) => (
            <TreeItem key={child.name} node={child} />
          ))}
        </div>
      </CollapsibleContent>
    </Collapsible>
  )
}

export default function CollapsibleFileTree() {
  return (
    <div className="w-full max-w-72 text-sm">
      {tree.map((node) => (
        <TreeItem key={node.name} node={node} defaultOpen={node.name === 'src'} />
      ))}
    </div>
  )
}
```

### Settings panel [#settings-panel]

Reveal extra controls only when they're needed. A single radius sets every corner; the icon-button beside it expands a per-corner panel below. The icon swaps between a maximize and minimize glyph entirely from the root's `data-open` state — no JavaScript.

```tsx
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@appica/ui-react/collapsible'
import { Button } from '@appica/ui-react/button'
import { Field, FieldLabel } from '@appica/ui-react/field'
import { Input } from '@appica/ui-react/input'
import { Maximize, Minimize } from '@appica/icons-react'

const px = <span className="text-foreground-muted text-xs">px</span>

function CornerField({ label, defaultValue }: { label: string; defaultValue: string }) {
  return (
    <Field>
      <FieldLabel className="text-xs font-normal">{label}</FieldLabel>
      <Input inputSize="sm" type="number" defaultValue={defaultValue} endSlot={px} />
    </Field>
  )
}

export default function CollapsibleSettingsPanel() {
  return (
    <Collapsible className="group border-border w-full max-w-82 rounded-xl border p-5">
      <div>
        <h3 className="text-foreground-intense font-semibold">Corner radius</h3>
        <p className="text-foreground-muted text-sm">Round every corner at once, or set each one.</p>
      </div>

      <div className="mt-4 flex items-center gap-3">
        <Input
          inputSize="sm"
          type="number"
          defaultValue="12"
          endSlot={px}
          className="flex-1"
          aria-label="All corners"
        />
        <CollapsibleTrigger
          className="shrink-0"
          render={<Button variant="outline" size="icon-sm" aria-label="Set each corner individually" />}
        >
          <Maximize className="group-data-open:hidden" />
          <Minimize className="hidden group-data-open:block" />
        </CollapsibleTrigger>
      </div>

      <CollapsibleContent>
        <div className="mt-4 grid grid-cols-2 gap-4 px-1.5 pb-1.5">
          <CornerField label="Top left" defaultValue="12" />
          <CornerField label="Top right" defaultValue="12" />
          <CornerField label="Bottom left" defaultValue="12" />
          <CornerField label="Bottom right" defaultValue="12" />
        </div>
      </CollapsibleContent>
    </Collapsible>
  )
}
```

### Show more [#show-more]

Because the trigger and panel are independent, the trigger can sit *below* the always-visible text as a "Show more / Show less" toggle, with the label following the open state.

```tsx
'use client'

import * as React from 'react'
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@appica/ui-react/collapsible'

export default function CollapsibleShowMore() {
  const [open, setOpen] = React.useState(false)

  return (
    <Collapsible open={open} onOpenChange={setOpen} className="max-w-100 text-sm leading-relaxed">
      <p>
        Appica UI is a headless-first component library built on Base UI and Tailwind. It ships accessible primitives
        with sensible default styling you can override class by class.
      </p>
      <CollapsibleContent>
        <p className="pt-3">
          Every component supports right-to-left layouts, honours <code>prefers-reduced-motion</code>, and forwards refs
          and native attributes to the underlying element — so it drops into your design system without fighting it.
        </p>
      </CollapsibleContent>
      <CollapsibleTrigger className="text-primary mt-2 cursor-pointer text-sm font-medium hover:underline">
        {open ? 'Show less' : 'Show more'}
      </CollapsibleTrigger>
    </Collapsible>
  )
}
```

### Controlled [#controlled]

Drive the open state yourself with `open` + `onOpenChange`. Here an external button and the header share the same state, so either one toggles the panel.

```tsx
'use client'

import * as React from 'react'
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@appica/ui-react/collapsible'
import { Button } from '@appica/ui-react/button'
import { ChevronRight } from '@appica/icons-react'

export default function CollapsibleControlled() {
  const [open, setOpen] = React.useState(false)

  return (
    <div className="flex w-full max-w-100 flex-col gap-3">
      <div className="flex items-center justify-between">
        <span className="text-foreground-muted text-sm">
          Panel is <strong className="text-foreground">{open ? 'open' : 'closed'}</strong>
        </span>
        <Button size="sm" variant="outline" onClick={() => setOpen((o) => !o)}>
          {open ? 'Collapse' : 'Expand'}
        </Button>
      </div>

      <Collapsible open={open} onOpenChange={setOpen}>
        <CollapsibleTrigger className="group text-foreground-intense inline-flex items-start gap-2 text-start font-medium">
          <ChevronRight className="mt-0.75 size-4.5 shrink-0 stroke-2 transition-transform duration-200 group-data-panel-open:rotate-90 motion-reduce:transition-none" />
          Order #4982
        </CollapsibleTrigger>
        <CollapsibleContent>
          <p className="ps-6.5 pt-2.5 text-sm">
            Two items, shipped on Aug&nbsp;30 via standard delivery. The header and the external button drive the same
            state, so either one toggles the panel.
          </p>
        </CollapsibleContent>
      </Collapsible>
    </div>
  )
}
```

### Disabled [#disabled]

Add `disabled` to the root to lock the panel shut. The trigger drops out of the tab order and exposes `data-disabled` for styling.

```tsx
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@appica/ui-react/collapsible'
import { ChevronRight } from '@appica/icons-react'

export default function CollapsibleDisabled() {
  return (
    <Collapsible disabled className="w-full max-w-100">
      <CollapsibleTrigger className="group text-foreground-intense data-disabled:text-foreground-muted inline-flex items-start gap-2 text-start font-medium">
        <ChevronRight className="mt-0.75 size-4.5 shrink-0 stroke-2 transition-transform duration-200 group-data-panel-open:rotate-90 motion-reduce:transition-none" />
        Advanced settings
      </CollapsibleTrigger>
      <CollapsibleContent>
        <p className="ps-6.5 pt-2.5 text-sm">These settings unlock on a paid plan.</p>
      </CollapsibleContent>
    </Collapsible>
  )
}
```

### Keep mounted [#keep-mounted]

By default the panel unmounts when closed. Pass `keepMounted` to leave it in the DOM so the browser's find-in-page and search-engine crawlers can still reach the hidden text.

```tsx
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@appica/ui-react/collapsible'
import { ChevronRight } from '@appica/icons-react'

export default function CollapsibleKeepMounted() {
  return (
    <Collapsible className="w-full max-w-100">
      <CollapsibleTrigger className="group text-foreground-intense inline-flex items-start gap-2 text-start font-medium">
        <ChevronRight className="mt-0.75 size-4.5 shrink-0 stroke-2 transition-transform duration-200 group-data-panel-open:rotate-90 motion-reduce:transition-none" />
        Shipping &amp; returns
      </CollapsibleTrigger>
      <CollapsibleContent keepMounted>
        <p className="ps-6.5 pt-2.5 text-sm">
          Orders ship within one business day and arrive in 3-5 days. With <code>keepMounted</code> the panel stays in
          the DOM while closed, so search engines and the browser's find-in-page can still reach this text.
        </p>
      </CollapsibleContent>
    </Collapsible>
  )
}
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your tree in `DirectionProvider` so direction-aware behavior follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

The trigger and its panel align to the right, the panel's indent moves to the right via the logical `ps-*` padding, and the leading chevron mirrors to point toward the inline start. For setup details and caveats, see the [RTL guide](/ui/docs/react/rtl).

<RtlPreview component="collapsible" />

## API reference [#api-reference]

`Collapsible` wraps [Base UI's Collapsible](https://base-ui.com/react/components/collapsible). Each part forwards `ref` and its remaining props to the matching Base UI primitive, so anything it accepts works here — the tables below list the most common props. The panel exposes the `--collapsible-panel-height` CSS variable that powers the height animation. See the Base UI docs for the complete API.

### Collapsible [#collapsible]

The root. Renders a `<div>` and owns the open state.

| Prop           | <ColMinWidth width="210">Type</ColMinWidth>                   | Default | <ColMinWidth width="220">Description</ColMinWidth> |
| -------------- | ------------------------------------------------------------- | ------- | -------------------------------------------------- |
| `defaultOpen`  | `boolean`                                                     | `false` | Uncontrolled initial open state.                   |
| `open`         | `boolean`                                                     | —       | Controlled open state. Pair with `onOpenChange`.   |
| `onOpenChange` | <Code>(open: boolean, details) => void</Code>                 | —       | Fires when the panel opens or closes.              |
| `disabled`     | `boolean`                                                     | `false` | Disable the trigger so the panel can't toggle.     |
| `render`       | <Code>ReactElement \| ((props, state) => ReactElement)</Code> | —       | Replace the root element, or compose it.           |
| `className`    | <Code>string \| ((state) => string)</Code>                    | —       | Extra classes, merged via `tailwind-merge`.        |

### CollapsibleTrigger [#collapsibletrigger]

The toggle button. Renders a `<button>` with `aria-expanded`/`aria-controls` wired to the panel, and exposes `data-panel-open` / `data-disabled`.

| Prop        | <ColMinWidth width="210">Type</ColMinWidth>                   | Default | <ColMinWidth width="220">Description</ColMinWidth>     |
| ----------- | ------------------------------------------------------------- | ------- | ------------------------------------------------------ |
| `children`  | `ReactNode`                                                   | —       | The trigger label and any indicator icon.              |
| `render`    | <Code>ReactElement \| ((props, state) => ReactElement)</Code> | —       | Render through another element (e.g. a custom button). |
| `className` | <Code>string \| ((state) => string)</Code>                    | —       | Extra classes, merged via `tailwind-merge`.            |

### CollapsibleContent [#collapsiblecontent]

The animated panel. Renders a `<div>` and exposes `data-open` / `data-closed`.

| Prop               | <ColMinWidth width="210">Type</ColMinWidth>                   | Default | <ColMinWidth width="220">Description</ColMinWidth>                   |
| ------------------ | ------------------------------------------------------------- | ------- | -------------------------------------------------------------------- |
| `children`         | `ReactNode`                                                   | —       | The panel content.                                                   |
| `keepMounted`      | `boolean`                                                     | `false` | Keep the panel in the DOM while closed (find-in-page / SEO).         |
| `hiddenUntilFound` | `boolean`                                                     | `false` | Use the native `hidden="until-found"` so closed text stays findable. |
| `render`           | <Code>ReactElement \| ((props, state) => ReactElement)</Code> | —       | Replace the panel element.                                           |
| `className`        | <Code>string \| ((state) => string)</Code>                    | —       | Extra classes, merged via `tailwind-merge`.                          |

## Accessibility [#accessibility]

* `CollapsibleTrigger` is a real `<button>` with `aria-expanded` reflecting the state and `aria-controls` pointing at the panel, so assistive tech announces "expanded"/"collapsed".
* **Enter** and **Space** toggle the panel while the trigger is focused; the trigger is reachable with **Tab**.
* A `disabled` collapsible removes the trigger from the tab order and marks it `data-disabled` for styling.
* Any indicator icon (the chevron in these examples) is decorative — the trigger's text label carries the meaning.
* The panel's height animation honours `prefers-reduced-motion`, collapsing instantly when the user prefers reduced motion.
