---
title: Collapsible
description: Reveal or hide supporting content in place.
group: Components
order: 26
---

Keep supporting details out of the way until they are needed. Pair a trigger with content, and nest panels for tree-like layouts.

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

function ChevronDownIcon(props: React.SVGProps<SVGSVGElement>) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}>
      <path d="m6 9 6 6 6-6" />
    </svg>
  )
}

export function Example() {
  return (
    <Card className="w-full py-0">
      <CardContent className="py-4">
        <Collapsible className="flex w-full flex-col">
          <div className="flex items-center justify-between gap-3">
            <h4 className="text-sm font-medium tracking-wide">
              Registry push #2841
            </h4>
            <CollapsibleTrigger className="group gap-1.5 rounded-md px-2.5 py-1.5 hover:bg-accent hover:text-accent-foreground">
              Details
              <ChevronDownIcon className="size-4 transition-transform duration-200 ease-out group-data-[state=open]:rotate-180" />
            </CollapsibleTrigger>
          </div>
          <CollapsibleContent className="flex flex-col gap-2 pt-3 text-sm tracking-wide text-muted-foreground">
            <div className="flex items-center justify-between gap-3 rounded-md border border-border px-3 py-2">
              <span>Status</span>
              <span className="text-foreground">Shipped</span>
            </div>
            <div className="flex items-center justify-between gap-3 rounded-md border border-border px-3 py-2">
              <span>Namespace</span>
              <span className="text-foreground">@arctis-sh/ui</span>
            </div>
          </CollapsibleContent>
        </Collapsible>
      </CardContent>
    </Card>
  )
}
```

## Installation

```bash
npx @arctis-sh/@arctis-sh/ui@latest add collapsible
```

## Usage

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

```tsx
<Collapsible>
  <CollapsibleTrigger>What’s included?</CollapsibleTrigger>
  <CollapsibleContent>
    You get the source. Install the component, then adapt it to your app.
  </CollapsibleContent>
</Collapsible>
```

## Composition

```tree
Collapsible
├── CollapsibleTrigger
└── CollapsibleContent
```

## Basic

A card header with a trigger that reveals status rows beneath the title.

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

function ChevronDownIcon(props: React.SVGProps<SVGSVGElement>) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}>
      <path d="m6 9 6 6 6-6" />
    </svg>
  )
}

export function Example() {
  return (
    <Card className="w-full py-0">
      <CardContent className="py-4">
        <Collapsible className="flex w-full flex-col">
          <div className="flex items-center justify-between gap-3">
            <h4 className="text-sm font-medium tracking-wide">
              Registry push #2841
            </h4>
            <CollapsibleTrigger className="group gap-1.5 rounded-md px-2.5 py-1.5 hover:bg-accent hover:text-accent-foreground">
              Details
              <ChevronDownIcon className="size-4 transition-transform duration-200 ease-out group-data-[state=open]:rotate-180" />
            </CollapsibleTrigger>
          </div>
          <CollapsibleContent className="flex flex-col gap-2 pt-3 text-sm tracking-wide text-muted-foreground">
            <div className="flex items-center justify-between gap-3 rounded-md border border-border px-3 py-2">
              <span>Status</span>
              <span className="text-foreground">Shipped</span>
            </div>
            <div className="flex items-center justify-between gap-3 rounded-md border border-border px-3 py-2">
              <span>Namespace</span>
              <span className="text-foreground">@arctis-sh/ui</span>
            </div>
          </CollapsibleContent>
        </Collapsible>
      </CardContent>
    </Card>
  )
}
```

## Controlled

Pass `open` and `onOpenChange` when the parent owns the state. Use `defaultOpen` to set an initial uncontrolled state.

```tsx
import * as React from "react"
import { Card, CardContent } from "@/components/ui/card"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible"

function ChevronDownIcon(props: React.SVGProps<SVGSVGElement>) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}>
      <path d="m6 9 6 6 6-6" />
    </svg>
  )
}

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

  return (
    <Card className="w-full bg-transparent py-0">
      <CardContent className="py-4">
        <Collapsible
          open={open}
          onOpenChange={setOpen}
          className="flex w-full flex-col"
        >
          <CollapsibleTrigger className="group w-full justify-between text-left">
            Theme tokens {open ? "(open)" : "(closed)"}
            <ChevronDownIcon className="size-4 transition-transform duration-200 ease-out group-data-[state=open]:rotate-180" />
          </CollapsibleTrigger>
          <CollapsibleContent className="pt-3 text-sm tracking-wide text-muted-foreground">
            CSS variables under globals.css drive light and dark surfaces for
            every component page.
          </CollapsibleContent>
        </Collapsible>
      </CardContent>
    </Card>
  )
}
```

## Disabled

Set `disabled` on the root to lock the trigger. Dim the row as well so it reads as inactive.

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

function ChevronDownIcon(props: React.SVGProps<SVGSVGElement>) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}>
      <path d="m6 9 6 6 6-6" />
    </svg>
  )
}

export function Example() {
  return (
    <Card className="w-full bg-transparent py-0">
      <CardContent className="py-4">
        <Collapsible disabled className="flex w-full flex-col">
          <CollapsibleTrigger className="group w-full justify-between text-left">
            Publish package — unlock after review
            <ChevronDownIcon className="size-4" />
          </CollapsibleTrigger>
          <CollapsibleContent className="pt-3 text-sm tracking-wide text-muted-foreground">
            Shipping is locked until the changelog entry is approved.
          </CollapsibleContent>
        </Collapsible>
      </CardContent>
    </Card>
  )
}
```

## Settings panel

Place a compact trigger beside a label, then reveal the related controls below it.

```tsx
import * as React from "react"
import { Card, CardContent } from "@/components/ui/card"
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible"

function CornersIcon(props: React.SVGProps<SVGSVGElement>) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}>
      <path d="M8 3H5a2 2 0 0 0-2 2v3" />
      <path d="M16 3h3a2 2 0 0 1 2 2v3" />
      <path d="M8 21H5a2 2 0 0 1-2-2v-3" />
      <path d="M16 21h3a2 2 0 0 0 2-2v-3" />
    </svg>
  )
}

function ShrinkCornersIcon(props: React.SVGProps<SVGSVGElement>) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}>
      <g transform="rotate(180 5.5 5.5)">
        <path d="M8 3H5a2 2 0 0 0-2 2v3" />
      </g>
      <g transform="rotate(180 18.5 5.5)">
        <path d="M16 3h3a2 2 0 0 1 2 2v3" />
      </g>
      <g transform="rotate(180 5.5 18.5)">
        <path d="M8 21H5a2 2 0 0 1-2-2v-3" />
      </g>
      <g transform="rotate(180 18.5 18.5)">
        <path d="M16 21h3a2 2 0 0 0 2-2v-3" />
      </g>
    </svg>
  )
}

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

  return (
    <Card className="w-full bg-transparent py-0">
      <CardContent className="py-4">
        <Collapsible
          open={open}
          onOpenChange={setOpen}
          className="flex w-full flex-col gap-3"
        >
          <div className="grid gap-1">
            <p className="text-sm font-medium tracking-wide">Radius</p>
            <p className="text-sm text-muted-foreground">
              Set the corner radius of the element.
            </p>
          </div>
          <div className="flex items-start gap-2">
            <div className="min-w-0 flex-1">
              <div className="grid grid-cols-2 gap-2">
                <input defaultValue="0" className="h-9 w-full rounded-md border border-border bg-transparent px-3 text-sm tracking-wide" />
                <input defaultValue="0" className="h-9 w-full rounded-md border border-border bg-transparent px-3 text-sm tracking-wide" />
              </div>
              <CollapsibleContent className="pt-2">
                <div className="grid grid-cols-2 gap-2">
                  <input defaultValue="0" className="h-9 w-full rounded-md border border-border bg-transparent px-3 text-sm tracking-wide" />
                  <input defaultValue="0" className="h-9 w-full rounded-md border border-border bg-transparent px-3 text-sm tracking-wide" />
                </div>
              </CollapsibleContent>
            </div>
            <CollapsibleTrigger className="inline-flex size-9 shrink-0 items-center justify-center rounded-md border border-border hover:bg-accent hover:text-accent-foreground">
              {open ? (
                <ShrinkCornersIcon className="size-4" />
              ) : (
                <CornersIcon className="size-4" />
              )}
              <span className="sr-only">Toggle radius settings</span>
            </CollapsibleTrigger>
          </div>
        </Collapsible>
      </CardContent>
    </Card>
  )
}
```

## File tree

Nest collapsibles to build a folder tree. The chevron rotates as each branch opens.

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

function ChevronRightIcon(props: React.SVGProps<SVGSVGElement>) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}>
      <path d="m9 18 6-6-6-6" />
    </svg>
  )
}

function FolderIcon(props: React.SVGProps<SVGSVGElement>) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}>
      <path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.64 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
    </svg>
  )
}

function FileIcon(props: React.SVGProps<SVGSVGElement>) {
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}>
      <path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" />
      <path d="M14 2v4a2 2 0 0 0 2 2h4" />
    </svg>
  )
}

function TreeFolder({
  name,
  defaultOpen = false,
  children,
}: {
  name: string
  defaultOpen?: boolean
  children: ReactNode
}) {
  return (
    <Collapsible defaultOpen={defaultOpen} className="flex flex-col">
      <CollapsibleTrigger className="group w-fit justify-start gap-1.5 rounded-md px-1.5 py-1 text-left text-sm tracking-wide hover:bg-accent hover:text-accent-foreground">
        <ChevronRightIcon className="size-3.5 shrink-0 transition-transform duration-200 ease-out group-data-[state=open]:rotate-90" />
        <FolderIcon className="size-3.5 shrink-0 text-muted-foreground" />
        {name}
      </CollapsibleTrigger>
      <CollapsibleContent className="ms-3 flex flex-col gap-1 border-l border-border ps-2 pt-1">
        {children}
      </CollapsibleContent>
    </Collapsible>
  )
}

function TreeFile({ name }: { name: string }) {
  return (
    <div className="inline-flex w-fit items-center gap-1.5 rounded-md px-1.5 py-1 text-sm tracking-wide text-muted-foreground hover:bg-accent hover:text-accent-foreground">
      <span className="size-3.5 shrink-0" />
      <FileIcon className="size-3.5 shrink-0" />
      {name}
    </div>
  )
}

export function Example() {
  return (
    <div className="w-full rounded-md border border-border p-3">
      <p className="mb-2 px-1.5 text-xs tracking-wide text-muted-foreground">
        Explorer
      </p>
      <TreeFolder name="components" defaultOpen>
        <TreeFolder name="ui" defaultOpen>
          <TreeFile name="button.tsx" />
          <TreeFile name="card.tsx" />
          <TreeFile name="collapsible.tsx" />
        </TreeFolder>
        <TreeFile name="logo.tsx" />
      </TreeFolder>
      <TreeFolder name="lib">
        <TreeFile name="utils.ts" />
      </TreeFolder>
      <TreeFile name="globals.css" />
      <TreeFile name="package.json" />
    </div>
  )
}
```

## API Reference

### Collapsible

| Prop | Type | Default |
| --- | --- | --- |
| open | boolean | — |
| defaultOpen | boolean | false |
| onOpenChange | (open: boolean) => void | — |
| disabled | boolean | false |
| className | string | — |

```tsx
<Collapsible>
  <CollapsibleTrigger>Toggle</CollapsibleTrigger>
  <CollapsibleContent>Hidden content</CollapsibleContent>
</Collapsible>
```

### CollapsibleTrigger

| Prop | Type | Default |
| --- | --- | --- |
| disabled | boolean | false |
| className | string | — |

```tsx
<CollapsibleTrigger>Toggle</CollapsibleTrigger>
```

### CollapsibleContent

| Prop | Type | Default |
| --- | --- | --- |
| unmountOnExit | boolean | false |
| className | string | — |

```tsx
<CollapsibleContent>Hidden content</CollapsibleContent>
```

## Source

```tsx
"use client";

import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
  type ButtonHTMLAttributes,
  type HTMLAttributes,
  type ReactNode,
} from "react";
import { cn } from "@/lib/utils";

type CollapsibleContextValue = {
  open: boolean;
  setOpen: (open: boolean) => void;
  disabled: boolean;
  triggerId: string;
  contentId: string;
};

const CollapsibleContext = createContext<CollapsibleContextValue | null>(null);

function useCollapsible() {
  const context = useContext(CollapsibleContext);
  if (!context) {
    throw new Error("Collapsible parts must be used within <Collapsible>");
  }
  return context;
}

type CollapsibleProps = HTMLAttributes<HTMLDivElement> & {
  open?: boolean;
  defaultOpen?: boolean;
  onOpenChange?: (open: boolean) => void;
  disabled?: boolean;
  children: ReactNode;
};

export function Collapsible({
  open: openProp,
  defaultOpen = false,
  onOpenChange,
  disabled = false,
  className,
  children,
  ...props
}: CollapsibleProps) {
  const baseId = useId();
  const triggerId = `${baseId}-trigger`;
  const contentId = `${baseId}-content`;
  const controlled = openProp !== undefined;
  const [uncontrolled, setUncontrolled] = useState(defaultOpen);
  const open = controlled ? openProp : uncontrolled;

  const setOpen = useCallback(
    (next: boolean) => {
      if (disabled) return;
      if (!controlled) setUncontrolled(next);
      onOpenChange?.(next);
    },
    [controlled, disabled, onOpenChange],
  );

  const value = useMemo(
    () => ({ open, setOpen, disabled, triggerId, contentId }),
    [contentId, disabled, open, setOpen, triggerId],
  );

  return (
    <CollapsibleContext.Provider value={value}>
      <div
        {...props}
        data-slot="collapsible"
        data-state={open ? "open" : "closed"}
        data-disabled={disabled ? "" : undefined}
        className={cn(className)}
      >
        {children}
      </div>
    </CollapsibleContext.Provider>
  );
}

type CollapsibleTriggerProps = ButtonHTMLAttributes<HTMLButtonElement> & {
  children: ReactNode;
};

export function CollapsibleTrigger({
  className,
  children,
  onClick,
  disabled,
  ...props
}: CollapsibleTriggerProps) {
  const {
    open,
    setOpen,
    disabled: rootDisabled,
    triggerId,
    contentId,
  } = useCollapsible();
  const isDisabled = disabled || rootDisabled;

  return (
    <button
      type="button"
      {...props}
      id={triggerId}
      data-slot="collapsible-trigger"
      data-state={open ? "open" : "closed"}
      aria-controls={contentId}
      aria-expanded={open}
      disabled={isDisabled}
      className={cn(
        "inline-flex items-center gap-2 text-sm font-normal tracking-wide transition-colors duration-200 ease-out disabled:pointer-events-none disabled:opacity-40",
        className,
      )}
      onClick={(event) => {
        onClick?.(event);
        if (!event.defaultPrevented) setOpen(!open);
      }}
    >
      {children}
    </button>
  );
}

type CollapsibleContentProps = HTMLAttributes<HTMLDivElement> & {
  unmountOnExit?: boolean;
  children: ReactNode;
};

export function CollapsibleContent({
  className,
  children,
  unmountOnExit = false,
  ...props
}: CollapsibleContentProps) {
  const { open, triggerId, contentId } = useCollapsible();
  const [mounted, setMounted] = useState(open || !unmountOnExit);
  const [mountKey, setMountKey] = useState(0);
  const wasOpen = useRef(open);

  useEffect(() => {
    if (!unmountOnExit) {
      setMounted(true);
      return;
    }

    if (open) {
      if (!wasOpen.current) {
        setMountKey((key) => key + 1);
      }
      wasOpen.current = true;
      setMounted(true);
      return;
    }

    wasOpen.current = false;
    const timeout = window.setTimeout(() => setMounted(false), 200);
    return () => window.clearTimeout(timeout);
  }, [open, unmountOnExit]);

  return (
    <div
      id={contentId}
      role="region"
      aria-labelledby={triggerId}
      data-slot="collapsible-content"
      data-state={open ? "open" : "closed"}
      className={cn(
        "grid transition-[grid-template-rows] duration-200 ease-out",
        open ? "grid-rows-[1fr]" : "grid-rows-[0fr]",
      )}
      {...props}
    >
      <div className="min-h-0 overflow-hidden" aria-hidden={!open}>
        {mounted ? (
          <div key={mountKey} className={cn(className)}>
            {children}
          </div>
        ) : null}
      </div>
    </div>
  );
}
```
