Skip to content
Drivn
5 min read

React Collapsible Component Examples

Drop-in React Collapsible examples: file tree, settings panel, controlled, default open, chevron rotation. Grid-rows animation, zero runtime deps.

A collapsible is the smallest disclosure widget there is — one trigger, one panel, one boolean — and it keeps getting rewritten because of the animation. Sliding a panel open means animating toward a height nobody knows until render, and the usual workarounds are a max-height guess that clips tall content or a ResizeObserver that measures on every frame. The Drivn Collapsible uses neither. Its panel is a CSS grid whose single row transitions between 0fr and 1fr, and browsers interpolate fractional units natively, so content of any height opens smoothly in 200ms from two lines of styles.

What lands in your repo is one file — the root plus Collapsible.Trigger and Collapsible.Content — built on React.useState, React.useId, and a single context, with cn as the only import that is not React. No Radix, no keyframes to register in a Tailwind config, nothing to configure before the first render.

The markup stays out of your way. The root is a <div> carrying data-state="open" or data-state="closed", the trigger is a real <button> wired to aria-expanded and aria-controls, and the content is a <div role="region"> labelled by that trigger — the WAI-ARIA disclosure pattern, without you matching ids by hand. Styling is entirely yours through className on each part. Five patterns follow, each copy-paste ready against the source the Drivn CLI installs: a basic toggle, a panel that starts open with defaultOpen, a nested file tree, a settings card with chevron rotation driven by data-state, and a controlled panel driven by parent state.

Basic Collapsible

Three tags is the whole component: root, trigger, content. The root owns the boolean and hands it down through context, the trigger is a <button> whose onClick calls toggle, and the content is the grid that opens. Nothing in between needs wiring, and nothing in the rendered output assumes a layout — drop the tree into a flex row, a sidebar item, or a table cell and it fits.

Two style rules do the animation, and they are the only styles the component brings. The outer panel is grid transition-[grid-template-rows] duration-200 with an inline gridTemplateRows of 1fr when open and 0fr when closed. The inner row is overflow-hidden, so content is clipped while that row shrinks to nothing. Everything else — padding, borders, backgrounds, spacing between rows — comes from the className you pass, which the Collapsible source merges onto the inner row rather than onto the grid itself. That split is why the panel below can carry space-y-2 without breaking the transition.

1import { Collapsible } from "@/components/ui/collapsible"
2import { ChevronsUpDown } from "lucide-react"
3
4export default function Page() {
5 return (
6 <Collapsible className="space-y-2">
7 <div className="flex items-center justify-between">
8 <h4 className="text-sm font-semibold">3 items</h4>
9 <Collapsible.Trigger>
10 <ChevronsUpDown className="w-4 h-4" />
11 </Collapsible.Trigger>
12 </div>
13 <div className="rounded-md border px-4 py-2.5 text-sm">
14 Always visible item
15 </div>
16 <Collapsible.Content className="space-y-2">
17 <div className="rounded-md border px-4 py-2.5 text-sm">
18 Hidden item 1
19 </div>
20 <div className="rounded-md border px-4 py-2.5 text-sm">
21 Hidden item 2
22 </div>
23 </Collapsible.Content>
24 </Collapsible>
25 )
26}

Default open

Pass defaultOpen for an uncontrolled Collapsible that starts in the open state. The Drivn root stores the state internally via useState(defaultOpen ?? false) and updates it on every toggle without notifying the parent unless you also pass onOpenChange. Use this shape for "show by default" disclosure rows where you want the content visible on first paint but the user can still collapse it.

A common variant is a "summary plus details" row — a header that always shows the summary count or label, with the full breakdown collapsed into the panel. Set defaultOpen so the panel is open on first paint, drop your styled rows above and below, and the user can click the trigger to fold the details away. The grid-rows animation handles the open-and-close transitions without any extra wiring.

1<Collapsible defaultOpen className="space-y-2">
2 <div className="flex items-center justify-between">
3 <h4 className="text-sm font-semibold">
4 @drivn/ui has 3 packages
5 </h4>
6 <Collapsible.Trigger>
7 <ChevronsUpDown className="w-4 h-4" />
8 </Collapsible.Trigger>
9 </div>
10 <div className="rounded-md border px-4 py-2.5 text-sm font-mono">
11 @drivn/components
12 </div>
13 <Collapsible.Content className="space-y-2">
14 <div className="rounded-md border px-4 py-2.5 text-sm font-mono">
15 @drivn/cli
16 </div>
17 <div className="rounded-md border px-4 py-2.5 text-sm font-mono">
18 @drivn/mcp
19 </div>
20 </Collapsible.Content>
21</Collapsible>

File tree (nested Collapsibles)

Collapsibles compose. Drop a Collapsible inside the Collapsible.Content of another and the inner panel animates independently of the outer. Because each Collapsible has its own data-state attribute and its own grid-rows transition, the nested expand-collapse runs without any extra coordination. The pattern is the foundation for IDE-style file trees, nested navigation menus, and any tree-of-disclosures UI.

The trick is keeping the visual indentation consistent. Wrap each level's Collapsible.Content in className="pl-4" so child rows render four units further to the right than their parent. Use a custom <FolderTrigger> component for the row that holds the icon and folder name, and a <FileItem> for leaf rows that do not toggle. The Drivn Collapsible source is unchanged — the composition lives entirely in the consumer code.

1<Collapsible defaultOpen>
2 <FolderTrigger name="src" />
3 <Collapsible.Content className="pl-4">
4 <Collapsible defaultOpen>
5 <FolderTrigger name="app" />
6 <Collapsible.Content className="pl-4">
7 <Collapsible>
8 <FolderTrigger name="docs" />
9 <Collapsible.Content className="pl-4">
10 <FileItem name="page.tsx" />
11 <FileItem name="layout.tsx" />
12 </Collapsible.Content>
13 </Collapsible>
14 <FileItem name="page.tsx" />
15 <FileItem name="layout.tsx" />
16 </Collapsible.Content>
17 </Collapsible>
18 <Collapsible defaultOpen>
19 <FolderTrigger name="components" />
20 <Collapsible.Content className="pl-4">
21 <FileItem name="button.tsx" />
22 <FileItem name="collapsible.tsx" />
23 </Collapsible.Content>
24 </Collapsible>
25 </Collapsible.Content>
26</Collapsible>

Settings panel with chevron rotation

For settings UIs and preferences pages, render multiple Collapsibles stacked inside a card. Each row is its own Collapsible — independent state, independent animation. The Drivn root sets data-state={open ? 'open' : 'closed'} on its outer <div>, which means any descendant can target the open state via Tailwind's group-data-[state=open]/<name> variant. Use a named group on each row (group/notif, group/privacy, group/appearance) so multiple rows on the page do not interfere with each other.

The most common decoration is a chevron icon that rotates 180 degrees when the panel opens. Set className="transition-transform group-data-[state=open]/<name>:rotate-180" on the chevron inside the trigger and the rotation runs in lockstep with the panel's open state — no extra useState, no extra render. The pattern reads cleanly because all the state-driven styling lives in Tailwind variants on the className strings.

1<div className="rounded-lg border border-border bg-card">
2 <Collapsible className="group/notif border-b border-border">
3 <Collapsible.Trigger className="...">
4 <Bell className="w-4 h-4" />
5 Notifications
6 <ChevronDown className="... group-data-[state=open]/notif:rotate-180" />
7 </Collapsible.Trigger>
8 <Collapsible.Content className="...">
9 Manage your notification preferences.
10 </Collapsible.Content>
11 </Collapsible>
12 <Collapsible className="group/privacy border-b border-border">
13 <Collapsible.Trigger className="...">
14 <Shield className="w-4 h-4" />
15 Privacy
16 <ChevronDown className="... group-data-[state=open]/privacy:rotate-180" />
17 </Collapsible.Trigger>
18 <Collapsible.Content className="...">
19 Control your privacy and data settings.
20 </Collapsible.Content>
21 </Collapsible>
22 <Collapsible className="group/appearance">
23 <Collapsible.Trigger className="...">
24 <Palette className="w-4 h-4" />
25 Appearance
26 <ChevronDown className="... group-data-[state=open]/appearance:rotate-180" />
27 </Collapsible.Trigger>
28 <Collapsible.Content className="...">
29 Customize theme, font size, and layout.
30 </Collapsible.Content>
31 </Collapsible>
32</div>

Controlled Collapsible

Pass open and onOpenChange to control the Collapsible from parent state. The Drivn Collapsible source detects the controlled mode via const open = controlledOpen ?? internalOpen and skips its internal setState call, calling onOpenChange directly. Use controlled mode when the open state needs to drive other UI — toggling a sibling panel, lifting open state into a URL query param, or syncing multiple Collapsibles to behave as an exclusive group.

The API mirrors every other compound in Drivn — Dialog, Drawer, Dropdown, Popover all accept open + onOpenChange with the same controlled-vs-uncontrolled detection. Once you have wired controlled mode for one, the rest are mechanical.

1'use client'
2import { useState } from "react"
3import { Collapsible } from "@/components/ui/collapsible"
4import { ChevronsUpDown } from "lucide-react"
5
6export default function Page() {
7 const [open, setOpen] = useState(false)
8
9 return (
10 <Collapsible
11 open={open}
12 onOpenChange={setOpen}
13 className="space-y-2"
14 >
15 <div className="flex items-center justify-between">
16 <h4 className="text-sm font-semibold">
17 3 pinned dependencies
18 </h4>
19 <Collapsible.Trigger>
20 <ChevronsUpDown className="w-4 h-4" />
21 </Collapsible.Trigger>
22 </div>
23 <div className="rounded-md border px-4 py-2.5 text-sm font-mono">
24 react@19.1.0
25 </div>
26 <Collapsible.Content className="space-y-2">
27 <div className="rounded-md border px-4 py-2.5 text-sm font-mono">
28 next@16.1.6
29 </div>
30 <div className="rounded-md border px-4 py-2.5 text-sm font-mono">
31 typescript@5.8.3
32 </div>
33 </Collapsible.Content>
34 </Collapsible>
35 )
36}
Get started

Install Drivn in one command

Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.

npx drivn@latest create

Requires Node 18+. Works with npm, pnpm, and yarn.

Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

Frequently asked questions

No. The Collapsible imports React for useState, useId, useContext, and createContext, plus cn from @/utils/cn — that is the entire dependency surface. There is no @radix-ui/react-collapsible, no cva, no class-variance-authority, no clsx wrapper. The animation runs on the native grid-template-rows: 0fr → 1fr CSS transition that browsers interpolate without JavaScript.

The Drivn Collapsible source wraps the content in a grid container with transition-[grid-template-rows] duration-200, then toggles the inline style between gridTemplateRows: "0fr" and gridTemplateRows: "1fr". Browsers interpolate fr units smoothly, so the row height transitions from zero to its natural size over 200ms — no max-height hack, no ResizeObserver, no JavaScript animation loop. The inner row carries overflow-hidden so content is clipped while the row collapses.

Yes, via Tailwind's group-data-[state=open] variant. The Drivn root sets data-state={open ? "open" : "closed"} on the outer <div>, so add className="group/coll" to the root and className="transition-transform group-data-[state=open]/coll:rotate-180" to the chevron icon inside the trigger. The rotation runs in lockstep with the panel's open state — no extra useState, no extra render. Use named groups (group/notif, group/privacy) to keep multiple Collapsibles independent.

Drop a Collapsible inside the Collapsible.Content of another and the inner panel animates independently. Each Collapsible has its own state, its own data-state, and its own grid-rows transition, so nesting is purely a composition concern. Wrap each level's Collapsible.Content in className="pl-4" for visual indentation, render a custom FolderTrigger row for the toggleable rows and a FileItem row for leaf entries.

Yes. Pass open and onOpenChange and the Drivn root detects the controlled mode, skipping its internal useState update and calling onOpenChange on every toggle. Lift the state to a useState in the parent, sync it to a URL query param via useRouter from Next.js, or wire it into a global store — the Collapsible just renders whatever boolean you pass through open.