Shadcn Collapsible for React and Tailwind
Collapsible content in a vertical stack.
Total visits
Installation
bunx --bun shadcn@latest add https://kit.dev/r/collapsible.jsonpnpm dlx shadcn@latest add https://kit.dev/r/collapsible.jsonnpx shadcn@latest add https://kit.dev/r/collapsible.jsonyarn shadcn@latest add https://kit.dev/r/collapsible.jsonInstall the following dependencies:
bun add @ark-ui/react lucide-reactpnpm add @ark-ui/react lucide-reactnpm install @ark-ui/react lucide-reactyarn add @ark-ui/react lucide-reactAdd the following animations to your globals.css.
@theme inline {
--animate-expand: expand 0.2s ease-out;
--animate-collapse: collapse 0.2s ease-out;
@keyframes expand {
from { height: var(--collapsed-height, 0); }
to { height: var(--height); }
}
@keyframes collapse {
from { height: var(--height); }
to { height: var(--collapsed-height, 0); }
}
}Copy and paste the following code into your project.
"use client";
import {
Collapsible as ArkCollapsible,
useCollapsible as useArkCollapsible,
useCollapsibleContext as useArkCollapsibleContext,
} from "@ark-ui/react/collapsible";
import { ChevronDownIcon } from "lucide-react";
import type React from "react";
import { cn } from "@/lib/utils";
export const useCollapsible = useArkCollapsible;
export const useCollapsibleContext = useArkCollapsibleContext;
export const CollapsibleContext = ArkCollapsible.Context;
const collapsibleRootClassName = "group/collapsible";
const splitCollapsedMount = (props: {
collapsedHeight?: React.ComponentProps<
typeof ArkCollapsible.Root
>["collapsedHeight"];
lazyMount?: boolean;
unmountOnExit?: boolean;
}) => {
const { collapsedHeight, lazyMount = true, unmountOnExit = true } = props;
const hasCollapsedHeight =
collapsedHeight !== undefined && collapsedHeight !== null;
return {
collapsedHeight,
"data-partial-collapse": hasCollapsedHeight ? "" : undefined,
lazyMount: hasCollapsedHeight ? false : lazyMount,
unmountOnExit: hasCollapsedHeight ? false : unmountOnExit,
};
};
export const Collapsible = (
props: React.ComponentProps<typeof ArkCollapsible.Root>
) => {
const { collapsedHeight, lazyMount, unmountOnExit, className, ...rest } =
props;
const mount = splitCollapsedMount({
collapsedHeight,
lazyMount,
unmountOnExit,
});
return (
<ArkCollapsible.Root
className={cn(collapsibleRootClassName, className)}
data-slot="collapsible"
{...mount}
{...rest}
/>
);
};
export const CollapsibleRootProvider = (
props: React.ComponentProps<typeof ArkCollapsible.RootProvider>
) => {
const { className, ...rest } = props;
return (
<ArkCollapsible.RootProvider
className={cn(collapsibleRootClassName, className)}
data-slot="collapsible"
{...rest}
/>
);
};
export const CollapsibleTrigger = (
props: React.ComponentProps<typeof ArkCollapsible.Trigger>
) => {
const { className, ...rest } = props;
return (
<ArkCollapsible.Trigger
className={cn(
"cursor-pointer",
"data-disabled:pointer-events-none data-disabled:opacity-64",
"has-data-[slot=collapsible-indicator]:[button]:justify-between",
className
)}
data-slot="collapsible-trigger"
{...rest}
/>
);
};
export const CollapsibleContent = (
props: React.ComponentProps<typeof ArkCollapsible.Content>
) => {
const { className, children, ...rest } = props;
return (
<ArkCollapsible.Content
className={cn(
"h-(--collapsed-height)",
"group-data-partial-collapse/collapsible:h-full",
"transition-[height] duration-200",
"overflow-hidden",
"data-[state=open]:animate-expand",
"data-[state=closed]:animate-collapse",
"motion-reduce:animate-none! motion-reduce:transition-none!"
)}
data-slot="collapsible-content"
{...rest}
>
<div className={className}>{children}</div>
</ArkCollapsible.Content>
);
};
export const CollapsibleIndicator = (
props: React.ComponentProps<typeof ArkCollapsible.Indicator>
) => {
const { className, children, ...rest } = props;
return (
<ArkCollapsible.Indicator
className={cn("data-[state=open]:[&_svg]:rotate-180", className)}
data-slot="collapsible-indicator"
{...rest}
>
{children ?? (
<ChevronDownIcon
aria-hidden="true"
className="transition-transform duration-200 motion-reduce:transition-none!"
/>
)}
</ArkCollapsible.Indicator>
);
};Update the import paths to match your project setup.
For several labeled sections in one stack, use Accordion.
Anatomy
Collapsible
├── CollapsibleTrigger
│ └── CollapsibleIndicator
└── CollapsibleContentuseCollapsible is the machine hook for CollapsibleRootProvider. useCollapsibleContext / CollapsibleContext is in-tree.
Usage
import {
Collapsible,
CollapsibleContent,
CollapsibleIndicator,
CollapsibleTrigger,
} from "@/components/ui/collapsible";<Collapsible>
<CollapsibleTrigger asChild>
<Button variant="outline">
Details
<CollapsibleIndicator />
</Button>
</CollapsibleTrigger>
<CollapsibleContent>Hidden content</CollapsibleContent>
</Collapsible>shadcn.io defaults lazyMount and unmountOnExit to true (Ark: false). Setting collapsedHeight (or relying on partial collapse) forces both off so the peek stays in the DOM.
className on CollapsibleContent is applied to an inner wrapper. Height animation uses --height / --collapsed-height on the content part.
Controlled
Use open and onOpenChange. { open } is a boolean.
Default open
Root Provider
Use useCollapsible with CollapsibleRootProvider when you need the API outside the tree. Pass lazyMount, unmountOnExit, collapsedHeight, and other machine options to useCollapsible(), not to the provider.
open is the intended state (changes immediately). visible stays true while the exit animation plays.
Context
States
Disabled
Examples
Nested
Partial collapse
collapsedHeight keeps a slice of the panel visible. Interactive controls inside the collapsed region are made inert so they cannot be focused.
Collapsed interactives
Links, buttons, and inputs in the collapsed area are inert so keyboard users cannot tab to hidden controls.
Lazy mount
shadcn.io already sets lazyMount and unmountOnExit on the root. Content mounts on first open and leaves the DOM after the exit animation.
Guides
Open vs visible
| Property | Meaning |
|---|---|
open | Intended expanded state. Flips as soon as the trigger fires. |
visible | Content is on screen, including during the close animation. |
CSS variables
Set on the content part:
| CSS variable | Description |
|---|---|
--height | Measured height when open |
--width | Measured width when open |
--collapsed-height | Peek height from collapsedHeight |
--collapsed-width | Peek width from collapsedWidth |
Indicator
The default chevron rotates when data-state="open". Pass children to CollapsibleIndicator to replace it. Decorative icons should be aria-hidden.
API Reference
shadcn.io wraps Ark UI Collapsible. Defaults below are shadcn.io values. lazyMount and unmountOnExit default to true (Ark: false), except when collapsedHeight is set (both forced false).
asChild merges props onto a single child element.
Collapsible
Root. Renders a div.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Render the child element instead of a div. |
className | string | - | Class names on the root. |
collapsedHeight | string | number | - | Height when closed (peek). Forces lazyMount and unmountOnExit off. |
collapsedWidth | string | number | - | Width when closed. |
defaultOpen | boolean | false | Uncontrolled initial open state. |
dir | "ltr" | "rtl" | - | Text direction. Usually inherited from LocaleProvider. |
disabled | boolean | - | Disable the trigger. |
hideMode | "display-none" | "activity" | "display-none" | How to hide mounted-but-closed content. activity needs React 19+. |
id | string | - | Unique id for the machine. |
ids | Partial<{ root: string; content: string; trigger: string }> | - | Element ids for composition. |
lazyMount | boolean | true | Mount content the first time it opens. Forced false with collapsedHeight. |
onExitComplete | () => void | - | Called when the close animation finishes. |
onOpenChange | (details: OpenChangeDetails) => void | - | Open state changed. { open: boolean }. |
open | boolean | - | Controlled open state. |
unmountOnExit | boolean | true | Unmount after the close animation. Forced false with collapsedHeight. |
| Attribute | Description |
|---|---|
data-slot | collapsible |
data-scope | collapsible |
data-part | root |
data-state | "open" or "closed" |
data-partial-collapse | Present when collapsedHeight is set (shadcn.io) |
CollapsibleRootProvider
Takes the API from useCollapsible. Renders a div.
| Prop | Type | Default | Description |
|---|---|---|---|
value | UseCollapsibleReturn | required | Return value of useCollapsible(). |
asChild | boolean | false | Render the child element instead of a div. |
className | string | - | Class names on the root. |
Pass lazyMount, unmountOnExit, collapsedHeight, and other machine options to useCollapsible(), not to the provider.
CollapsibleTrigger
Toggles the panel. Renders a button. Use asChild with Button.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the trigger. |
| Attribute | Description |
|---|---|
data-slot | collapsible-trigger |
data-scope | collapsible |
data-part | trigger |
data-state | "open" or "closed" |
data-disabled | Present when disabled |
aria-expanded follows visibility. aria-controls points at the content.
CollapsibleContent
The expanding panel. Renders a div. className is applied to an inner wrapper so height animation can use --height.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the inner wrapper. |
| Attribute | Description |
|---|---|
data-slot | collapsible-content |
data-scope | collapsible |
data-part | content |
data-state | "open" or "closed" |
data-disabled | Present when disabled |
data-has-collapsed-size | Present when collapsedHeight or collapsedWidth is set |
| CSS variable | Description |
|---|---|
--height / --width | Measured open size |
--collapsed-height / --collapsed-width | Peek size |
CollapsibleIndicator
State-aware icon slot. Defaults to a chevron that rotates when open. Renders a div.
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Merge onto a single child. |
className | string | - | Class names on the indicator. |
children | ReactNode | chevron | Replace the default icon. |
| Attribute | Description |
|---|---|
data-slot | collapsible-indicator |
data-scope | collapsible |
data-part | indicator |
data-state | "open" or "closed" |
data-disabled | Present when disabled |
useCollapsible
Creates the collapsible API for CollapsibleRootProvider. Accepts the same machine options as Collapsible except layout-only props (className, asChild). To match the shadcn.io root, pass lazyMount: true and unmountOnExit: true.
const collapsible = useCollapsible({ lazyMount: true, unmountOnExit: true });
collapsible.setOpen(true);CollapsibleContext / useCollapsibleContext
Render-prop or hook access. Use inside Collapsible or CollapsibleRootProvider.
| Property | Type | Description |
|---|---|---|
open | boolean | Intended expanded state. |
visible | boolean | Content is on screen (including exit animation). |
disabled | boolean | Whether the collapsible is disabled. |
setOpen | (open: boolean) => void | Open or close. |
measureSize | () => void | Re-measure content size. |
CollapsibleContext children: (context) => ReactNode.
Accessibility
The trigger is a button with aria-expanded and aria-controls. Collapsed content is hidden from the tab order (inert when a collapsed size is set). Do not put the only copy of important actions solely inside a collapsed panel without a way to expand it from the keyboard.
Keyboard support
| Key | Description |
|---|---|
Tab | Move to the trigger, then into open content. |
Shift + Tab | Move to the previous control. |
Enter / Space | Toggle the panel when the trigger is focused. |