# Collapsible

> A panel that shows and hides with height motion you can reverse mid-way, without jumping the layout.

Docs: https://hextaui.com/docs/collapsible
Markdown: https://hextaui.com/docs/collapsible.md

```tsx title="components/examples/collapsible/demo.tsx"
import { Button } from "@/components/ui/button"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
  CollapsibleTriggerIcon,
} from "@/components/ui/collapsible"

export function CollapsibleDemo() {
  return (
    <Collapsible className="flex w-full max-w-sm flex-col gap-2">
      <div className="flex items-center justify-between gap-4 ps-4">
        <h4 className="text-sm font-semibold">
          <span dir="ltr">@preetsuthar17</span> starred 3 repositories
        </h4>
        <CollapsibleTrigger
          render={<Button variant="ghost" size="icon-sm" />}
          aria-label="Toggle repositories"
        >
          <CollapsibleTriggerIcon />
        </CollapsibleTrigger>
      </div>
      <div className="rounded-md border px-4 py-2 font-mono text-sm">
        <span dir="ltr">@radix-ui/primitives</span>
      </div>
      <CollapsibleContent className="flex flex-col gap-2">
        <div className="rounded-md border px-4 py-2 font-mono text-sm">
          <span dir="ltr">@base-ui/react</span>
        </div>
        <div className="rounded-md border px-4 py-2 font-mono text-sm">
          <span dir="ltr">@tabler/icons</span>
        </div>
      </CollapsibleContent>
    </Collapsible>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/collapsible.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @base-ui/react @tabler/icons-react cn
```

Copy and paste the following code into your project.

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

import * as React from "react"
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { IconChevronDown } from "@tabler/icons-react"
import { cn } from "cn"

type ClassName<State> =
  string | ((state: State) => string | undefined) | undefined

function mergeClassName<State>(base: string, className: ClassName<State>) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

function mergeRefs<T>(...refs: (React.Ref<T> | undefined)[]) {
  return (node: T) => {
    const cleanups = refs.map((ref) => {
      if (typeof ref === "function") {
        const cleanup = ref(node)
        return typeof cleanup === "function" ? cleanup : () => ref(null)
      }
      if (ref) {
        ref.current = node
        return () => {
          ref.current = null
        }
      }
      return undefined
    })
    return () => {
      for (const cleanup of cleanups) {
        cleanup?.()
      }
    }
  }
}

function getCollapsedGap(panel: HTMLElement) {
  const parent = panel.parentElement

  if (!parent) {
    return { start: 0, end: 0 }
  }

  const style = getComputedStyle(parent)
  const isFlexColumn =
    style.display.endsWith("flex") && style.flexDirection.startsWith("column")
  const isSingleColumnGrid =
    style.display.endsWith("grid") &&
    style.gridTemplateColumns.trim().split(/\s+/).length === 1
  const gap =
    isFlexColumn || isSingleColumnGrid ? parseFloat(style.rowGap) || 0 : 0

  if (!gap) {
    return { start: 0, end: 0 }
  }

  const reversed = isFlexColumn && style.flexDirection === "column-reverse"

  if (panel.previousElementSibling) {
    return reversed ? { start: 0, end: gap } : { start: gap, end: 0 }
  }

  if (panel.nextElementSibling) {
    return reversed ? { start: gap, end: 0 } : { start: 0, end: gap }
  }

  return { start: 0, end: 0 }
}

function syncCollapsedGap(panel: HTMLElement) {
  const { start, end } = getCollapsedGap(panel)
  const { style } = panel

  if (
    style.getPropertyValue("--collapsible-gap-start") === `${start}px` &&
    style.getPropertyValue("--collapsible-gap-end") === `${end}px`
  ) {
    return
  }

  const transition = style.getPropertyValue("transition")
  const priority = style.getPropertyPriority("transition")
  style.setProperty("transition", "none", "important")
  style.setProperty("--collapsible-gap-start", `${start}px`)
  style.setProperty("--collapsible-gap-end", `${end}px`)
  void getComputedStyle(panel).margin
  if (transition) {
    style.setProperty("transition", transition, priority)
  } else {
    style.removeProperty("transition")
  }
}

function Collapsible(props: CollapsiblePrimitive.Root.Props) {
  return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}

function CollapsibleTrigger({
  className,
  ...props
}: CollapsiblePrimitive.Trigger.Props) {
  return (
    <CollapsiblePrimitive.Trigger
      data-slot="collapsible-trigger"
      className={mergeClassName(
        "group/collapsible-trigger [-webkit-tap-highlight-color:transparent]",
        className
      )}
      {...props}
    />
  )
}

function CollapsibleTriggerIcon({
  className,
  children,
  render,
  ...props
}: useRender.ComponentProps<"span">) {
  return useRender({
    defaultTagName: "span",
    render,
    props: mergeProps<"span">(
      {
        "aria-hidden": true,
        className: cn(
          "inline-flex size-4 shrink-0 items-center justify-center transition-[translate] duration-150 group-active/collapsible-trigger:translate-y-px motion-reduce:transition-none [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
          className
        ),
        children: children ?? (
          <IconChevronDown className="transition-[rotate] duration-200 ease-spring group-data-panel-open/collapsible-trigger:rotate-180 motion-reduce:transition-none" />
        ),
      },
      props,
      { "data-slot": "collapsible-trigger-icon" } as Record<string, string>
    ),
  })
}

function CollapsibleContent({
  className,
  children,
  hiddenUntilFound = true,
  ref,
  ...props
}: Omit<CollapsiblePrimitive.Panel.Props, "className"> & {
  className?: string
}) {
  const observeGap = React.useCallback((panel: HTMLDivElement | null) => {
    const parent = panel?.parentElement

    if (!panel || !parent) {
      return
    }

    syncCollapsedGap(panel)

    const sync = () => syncCollapsedGap(panel)
    const resizeObserver =
      typeof ResizeObserver === "undefined" ? null : new ResizeObserver(sync)
    const mutationObserver = new MutationObserver(sync)
    resizeObserver?.observe(parent)
    mutationObserver.observe(parent, { childList: true })

    return () => {
      resizeObserver?.disconnect()
      mutationObserver.disconnect()
    }
  }, [])
  const setRef = React.useMemo(
    () => mergeRefs(observeGap, ref),
    [observeGap, ref]
  )

  return (
    <CollapsiblePrimitive.Panel
      data-slot="collapsible-content"
      ref={setRef}
      hiddenUntilFound={hiddenUntilFound}
      className="group/collapsible-content h-(--collapsible-panel-height) overflow-hidden transition-[height,margin] duration-300 ease-spring data-ending-style:h-0 data-ending-style:duration-150 data-starting-style:-mt-(--collapsible-gap-start) data-starting-style:-mb-(--collapsible-gap-end) data-starting-style:h-0 motion-reduce:transition-none data-closed:-mt-(--collapsible-gap-start) data-closed:-mb-(--collapsible-gap-end)"
      {...props}
    >
      <div
        data-slot="collapsible-content-inner"
        className={cn(
          "transition-[opacity,translate] duration-200 ease-out-quint group-data-ending-style/collapsible-content:-translate-y-1 group-data-ending-style/collapsible-content:opacity-0 group-data-ending-style/collapsible-content:duration-150 group-data-starting-style/collapsible-content:-translate-y-1 group-data-starting-style/collapsible-content:opacity-0 motion-reduce:transition-none",
          className
        )}
      >
        {children}
      </div>
    </CollapsiblePrimitive.Panel>
  )
}

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

Update the import paths to match your project setup.

## Usage

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

```tsx
<Collapsible>
  <CollapsibleTrigger render={<Button variant="outline" />}>
    Details
    <CollapsibleTriggerIcon data-icon="inline-end" />
  </CollapsibleTrigger>
  <CollapsibleContent>
    Shipping takes 2 business days.
  </CollapsibleContent>
</Collapsible>
```

## Composition

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

## Examples

### Open by default

`defaultOpen` starts the panel open without taking over its state.

```tsx title="components/examples/collapsible/open-by-default.tsx"
import { Button } from "@/components/ui/button"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
  CollapsibleTriggerIcon,
} from "@/components/ui/collapsible"

export function CollapsibleOpenByDefault() {
  return (
    <Collapsible defaultOpen className="flex w-full max-w-sm flex-col gap-2">
      <div className="flex items-center justify-between gap-4 ps-4">
        <h4 className="text-sm font-semibold">
          <span dir="ltr">@preetsuthar17</span> starred 3 repositories
        </h4>
        <CollapsibleTrigger
          render={<Button variant="ghost" size="icon-sm" />}
          aria-label="Toggle repositories"
        >
          <CollapsibleTriggerIcon />
        </CollapsibleTrigger>
      </div>
      <div className="rounded-md border px-4 py-2 font-mono text-sm">
        <span dir="ltr">@radix-ui/primitives</span>
      </div>
      <CollapsibleContent className="flex flex-col gap-2">
        <div className="rounded-md border px-4 py-2 font-mono text-sm">
          <span dir="ltr">@base-ui/react</span>
        </div>
        <div className="rounded-md border px-4 py-2 font-mono text-sm">
          <span dir="ltr">@tabler/icons</span>
        </div>
      </CollapsibleContent>
    </Collapsible>
  )
}
```

### Show more

The panel sits above its trigger. When it closes, the gap in the parent’s flex layout collapses in the same animation, so the button below never jumps.

```tsx title="components/examples/collapsible/show-more.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
  CollapsibleTriggerIcon,
} from "@/components/ui/collapsible"

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

  return (
    <Collapsible
      open={open}
      onOpenChange={setOpen}
      className="flex max-w-md flex-col gap-3"
    >
      <p className="text-sm text-muted-foreground">
        HextaUI components are built on Base UI primitives and styled with
        Tailwind. You own the source, so every class can be changed.
      </p>
      <CollapsibleContent>
        <p className="text-sm text-muted-foreground">
          Motion is interruptible: clicking again mid-animation reverses from
          where the panel is instead of snapping. The gap between this paragraph
          and its neighbours collapses in the same animation, so nothing below
          jumps when it finishes.
        </p>
      </CollapsibleContent>
      <div>
        <CollapsibleTrigger render={<Button variant="outline" size="sm" />}>
          {open ? "Show less" : "Show more"}
          <CollapsibleTriggerIcon data-icon="inline-end" />
        </CollapsibleTrigger>
      </div>
    </Collapsible>
  )
}
```

### Controlled

Pass `open` and `onOpenChange` to drive the panel from your own state. Toggling mid-animation reverses from where the panel is.

```tsx title="components/examples/collapsible/controlled.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
  CollapsibleTriggerIcon,
} from "@/components/ui/collapsible"

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

  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <div className="flex items-center gap-2">
        <Button variant="outline" size="sm" onClick={() => setOpen(true)}>
          Open
        </Button>
        <Button variant="outline" size="sm" onClick={() => setOpen(false)}>
          Close
        </Button>
        <span className="text-sm text-muted-foreground">
          {open ? "Open" : "Closed"}
        </span>
      </div>
      <Collapsible
        open={open}
        onOpenChange={setOpen}
        className="flex flex-col gap-2"
      >
        <div className="flex items-center justify-between gap-4 ps-4">
          <h4 className="text-sm font-semibold">Release notes</h4>
          <CollapsibleTrigger
            render={<Button variant="ghost" size="icon-sm" />}
            aria-label="Toggle release notes"
          >
            <CollapsibleTriggerIcon />
          </CollapsibleTrigger>
        </div>
        <div className="rounded-md border px-4 py-2 text-sm">v2.4.0</div>
        <CollapsibleContent className="flex flex-col gap-2">
          <div className="rounded-md border px-4 py-2 text-sm">v2.3.1</div>
          <div className="rounded-md border px-4 py-2 text-sm">v2.3.0</div>
        </CollapsibleContent>
      </Collapsible>
    </div>
  )
}
```

### Disabled

`disabled` on the root keeps the panel in its current state and makes the trigger ignore clicks and keys.

```tsx title="components/examples/collapsible/disabled.tsx"
import { Button } from "@/components/ui/button"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
  CollapsibleTriggerIcon,
} from "@/components/ui/collapsible"

export function CollapsibleDisabled() {
  return (
    <Collapsible disabled className="flex w-full max-w-sm flex-col gap-2">
      <div className="flex items-center justify-between gap-4 ps-4">
        <h4 className="text-sm font-semibold">
          <span dir="ltr">@preetsuthar17</span> starred 3 repositories
        </h4>
        <CollapsibleTrigger
          render={<Button variant="ghost" size="icon-sm" />}
          aria-label="Toggle repositories"
        >
          <CollapsibleTriggerIcon />
        </CollapsibleTrigger>
      </div>
      <div className="rounded-md border px-4 py-2 font-mono text-sm">
        <span dir="ltr">@radix-ui/primitives</span>
      </div>
      <CollapsibleContent className="flex flex-col gap-2">
        <div className="rounded-md border px-4 py-2 font-mono text-sm">
          <span dir="ltr">@base-ui/react</span>
        </div>
        <div className="rounded-md border px-4 py-2 font-mono text-sm">
          <span dir="ltr">@tabler/icons</span>
        </div>
      </CollapsibleContent>
    </Collapsible>
  )
}
```

### Custom icon

Children of `<CollapsibleTriggerIcon />` replace the chevron. Style the open state with `group-data-panel-open/collapsible-trigger`.

```tsx title="components/examples/collapsible/custom-icon.tsx"
import { IconPlus } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
  CollapsibleTriggerIcon,
} from "@/components/ui/collapsible"

export function CollapsibleCustomIcon() {
  return (
    <Collapsible className="flex w-full max-w-sm flex-col gap-2">
      <CollapsibleTrigger
        render={<Button variant="ghost" />}
        className="self-start"
      >
        <CollapsibleTriggerIcon data-icon="inline-start">
          <IconPlus className="transition-transform duration-200 ease-out-cubic group-data-panel-open/collapsible-trigger:rotate-45 motion-reduce:transition-none" />
        </CollapsibleTriggerIcon>
        Add a note
      </CollapsibleTrigger>
      <CollapsibleContent>
        <p className="ps-3 text-sm text-muted-foreground">
          Notes are only visible to you.
        </p>
      </CollapsibleContent>
    </Collapsible>
  )
}
```

### File tree

Each folder is its own collapsible. Outer panels grow smoothly as inner ones open.

```tsx title="components/examples/collapsible/file-tree.tsx"
import { IconChevronRight, IconFile, IconFolder } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
  CollapsibleTriggerIcon,
} from "@/components/ui/collapsible"

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

const tree: TreeNode[] = [
  {
    name: "components",
    children: [
      {
        name: "ui",
        children: [
          { name: "button.tsx" },
          { name: "collapsible.tsx" },
          { name: "card.tsx" },
        ],
      },
      { name: "theme-provider.tsx" },
    ],
  },
  { name: "app", children: [{ name: "layout.tsx" }, { name: "page.tsx" }] },
  { name: "package.json" },
]

function TreeItem({ node }: { node: TreeNode }) {
  if (!node.children) {
    return (
      <li className="flex h-8 items-center gap-2 ps-8 text-sm">
        <IconFile className="size-4 text-muted-foreground" />
        {node.name}
      </li>
    )
  }

  return (
    <li>
      <Collapsible defaultOpen={node.name === "components"}>
        <CollapsibleTrigger
          render={<Button variant="ghost" size="sm" />}
          className="w-full justify-start"
        >
          <CollapsibleTriggerIcon>
            <IconChevronRight className="transition-transform duration-200 ease-out-cubic group-data-panel-open/collapsible-trigger:rotate-90 motion-reduce:transition-none rtl:-scale-x-100 rtl:group-data-panel-open/collapsible-trigger:-rotate-90" />
          </CollapsibleTriggerIcon>
          <IconFolder className="text-muted-foreground" />
          {node.name}
        </CollapsibleTrigger>
        <CollapsibleContent>
          <ul className="ms-4 border-s ps-1">
            {node.children.map((child) => (
              <TreeItem key={child.name} node={child} />
            ))}
          </ul>
        </CollapsibleContent>
      </Collapsible>
    </li>
  )
}

export function CollapsibleFileTree() {
  return (
    <ul className="w-64">
      {tree.map((node) => (
        <TreeItem key={node.name} node={node} />
      ))}
    </ul>
  )
}
```

### Inside a card

Closed panels stay in the page with hidden=“until-found”, so Cmd/Ctrl+F for “webhook” opens this one.

```tsx title="components/examples/collapsible/card.tsx"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
  CollapsibleTriggerIcon,
} from "@/components/ui/collapsible"

export function CollapsibleCard() {
  return (
    <Card className="w-full max-w-md">
      <CardHeader>
        <CardTitle>Notifications</CardTitle>
        <CardDescription>Choose where alerts are sent.</CardDescription>
      </CardHeader>
      <CardContent>
        <Collapsible className="flex flex-col gap-3">
          <CollapsibleTrigger
            render={<Button variant="outline" />}
            className="justify-between"
          >
            Advanced settings
            <CollapsibleTriggerIcon data-icon="inline-end" />
          </CollapsibleTrigger>
          <CollapsibleContent className="flex flex-col gap-2 text-sm text-muted-foreground">
            <p>Send a webhook to your endpoint for every alert.</p>
            <p>Retry failed deliveries up to five times with backoff.</p>
            <p>Group alerts that arrive within 30 seconds.</p>
          </CollapsibleContent>
        </Collapsible>
      </CardContent>
    </Card>
  )
}
```

### Unmounted when closed

Set `hiddenUntilFound={false}` on the content to remove it from the DOM while closed, for heavy panels you don’t want rendered up front.

```tsx title="components/examples/collapsible/unmounted.tsx"
import { Button } from "@/components/ui/button"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
  CollapsibleTriggerIcon,
} from "@/components/ui/collapsible"

export function CollapsibleUnmounted() {
  return (
    <Collapsible className="flex w-full max-w-sm flex-col gap-2">
      <CollapsibleTrigger
        render={<Button variant="secondary" />}
        className="self-start"
      >
        Details
        <CollapsibleTriggerIcon data-icon="inline-end" />
      </CollapsibleTrigger>
      <CollapsibleContent hiddenUntilFound={false}>
        <p className="text-sm text-muted-foreground">
          Rendered only while open.
        </p>
      </CollapsibleContent>
    </Collapsible>
  )
}
```

### Long content

Unbroken strings wrap inside the panel instead of widening the page.

```tsx title="components/examples/collapsible/long-content.tsx"
import { Button } from "@/components/ui/button"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
  CollapsibleTriggerIcon,
} from "@/components/ui/collapsible"

export function CollapsibleLongContent() {
  return (
    <Collapsible className="flex w-72 max-w-full flex-col gap-2">
      <CollapsibleTrigger
        render={<Button variant="outline" />}
        className="justify-between"
      >
        Token
        <CollapsibleTriggerIcon data-icon="inline-end" />
      </CollapsibleTrigger>
      <CollapsibleContent className="font-mono text-sm wrap-anywhere">
        hx_demo_7Kq2vX9mLpR4tYw8NzB3cF6jHs1DgQe5UaV0iMoTnWbZrE2yPkLxJ4
      </CollapsibleContent>
    </Collapsible>
  )
}
```

### Right to left

Chevrons mirror and nested indents follow the reading direction.

```tsx title="components/examples/collapsible/rtl.tsx"
import { IconChevronRight, IconFile, IconFolder } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
  CollapsibleTriggerIcon,
} from "@/components/ui/collapsible"

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

const tree: TreeNode[] = [
  {
    name: "components",
    children: [
      {
        name: "ui",
        children: [
          { name: "button.tsx" },
          { name: "collapsible.tsx" },
          { name: "card.tsx" },
        ],
      },
      { name: "theme-provider.tsx" },
    ],
  },
  { name: "app", children: [{ name: "layout.tsx" }, { name: "page.tsx" }] },
  { name: "package.json" },
]

function TreeItem({ node }: { node: TreeNode }) {
  if (!node.children) {
    return (
      <li className="flex h-8 items-center gap-2 ps-8 text-sm">
        <IconFile className="size-4 text-muted-foreground" />
        {node.name}
      </li>
    )
  }

  return (
    <li>
      <Collapsible defaultOpen={node.name === "components"}>
        <CollapsibleTrigger
          render={<Button variant="ghost" size="sm" />}
          className="w-full justify-start"
        >
          <CollapsibleTriggerIcon>
            <IconChevronRight className="transition-transform duration-200 ease-out-cubic group-data-panel-open/collapsible-trigger:rotate-90 motion-reduce:transition-none rtl:-scale-x-100 rtl:group-data-panel-open/collapsible-trigger:-rotate-90" />
          </CollapsibleTriggerIcon>
          <IconFolder className="text-muted-foreground" />
          {node.name}
        </CollapsibleTrigger>
        <CollapsibleContent>
          <ul className="ms-4 border-s ps-1">
            {node.children.map((child) => (
              <TreeItem key={child.name} node={child} />
            ))}
          </ul>
        </CollapsibleContent>
      </Collapsible>
    </li>
  )
}

export function CollapsibleRtl() {
  return (
    <div dir="rtl" className="flex w-full max-w-sm flex-col gap-8">
      <Collapsible className="flex flex-col gap-2">
        <div className="flex items-center justify-between gap-4 ps-4">
          <h4 className="text-sm font-semibold">المستودعات المميزة</h4>
          <CollapsibleTrigger
            render={<Button variant="ghost" size="icon-sm" />}
            aria-label="إظهار المستودعات"
          >
            <CollapsibleTriggerIcon />
          </CollapsibleTrigger>
        </div>
        <div className="rounded-md border px-4 py-2 font-mono text-sm">
          <span dir="ltr">@radix-ui/primitives</span>
        </div>
        <CollapsibleContent className="flex flex-col gap-2">
          <div className="rounded-md border px-4 py-2 font-mono text-sm">
            <span dir="ltr">@base-ui/react</span>
          </div>
          <div className="rounded-md border px-4 py-2 font-mono text-sm">
            <span dir="ltr">@tabler/icons</span>
          </div>
        </CollapsibleContent>
      </Collapsible>
      <ul className="w-64">
        {tree.map((node) => (
          <TreeItem key={node.name} node={node} />
        ))}
      </ul>
    </div>
  )
}
```

## Keyboard

| Key | Action |
| --- | --- |
| `Enter` `Space` | Opens or closes the panel from the trigger. |
| `Tab` | Moves focus from the trigger into the open panel. Closed panels are skipped. |

## Accessibility

- The trigger gets `aria-expanded` and `aria-controls` automatically.
- Icon-only triggers need an `aria-label`. `<CollapsibleTriggerIcon />` is hidden from assistive tech.
- With reduced motion the panel opens and closes without animating its height.

## API reference

Built on the Base UI collapsible. Every part accepts the props of the primitive it wraps.

### Collapsible

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | – |  |
| `defaultOpen` | `boolean` | `false` |  |
| `onOpenChange` | `(open: boolean, details) => void` | – |  |
| `disabled` | `boolean` | `false` |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="collapsible"` | Target the root in CSS. |
| `data-open` | Present when open. |
| `data-closed` | Present when closed. |

### CollapsibleTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<button>` | Usually a Button. |

| Attribute | Description |
| --- | --- |
| `data-slot="collapsible-trigger"` | Target the trigger in CSS. |
| `data-panel-open` | Present when the panel is open. Style children with group-data-panel-open/collapsible-trigger. |

### CollapsibleTriggerIcon

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | `<IconChevronDown />` | The default chevron flips when the panel opens. |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<span>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="collapsible-trigger-icon"` | Hidden from assistive tech. Nudges down while the trigger is pressed. |

### CollapsibleContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | – | Applied to the inner wrapper, so padding never fights the height animation. |
| `hiddenUntilFound` | `boolean` | `true` | Keeps the closed panel in the DOM so page search can find and open it. |
| `keepMounted` | `boolean` | `false` | Ignored while hiddenUntilFound is on. |

| Attribute | Description |
| --- | --- |
| `data-slot="collapsible-content"` | Target the panel in CSS. |
| `data-open` | Present when open. |
| `data-closed` | Present when closed. |
| `data-starting-style` | Present while the panel animates in. |
| `data-ending-style` | Present while the panel animates out. |
| `--collapsible-panel-height` | The panel’s measured height. |
| `--collapsible-panel-width` | The panel’s measured width. |
| `--collapsible-gap-start` | The parent’s gap before the panel, collapsed while closed. |
| `--collapsible-gap-end` | The parent’s gap after the panel, collapsed while closed. |

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
