Skip to content
Drivn logoDrivn
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.

Collapsible panels show up everywhere — file trees, settings rows, FAQ-style disclosure, "show more" toggles, dependency lists in package viewers. The Drivn Collapsible component is a single compound primitive — Collapsible, Collapsible.Trigger, Collapsible.Content — built on React.useState, React.useId, and one React Context. About 100 lines of TSX with zero runtime dependencies beyond the local cn utility. The animation runs on the modern grid-template-rows: 0fr → 1fr CSS transition, so there is no max-height measurement, no JavaScript animation loop, and no Tailwind keyframe definitions to copy into tailwind.config.

This page collects the patterns that come up in production. Each snippet is copy-paste ready and stays consistent with the Collapsible source the Drivn CLI writes into your repo. The default render is unstyled — the root is just a <div> with data-state="open" or data-state="closed", the trigger is a <button> with the appropriate ARIA attributes, and the content is a grid container that animates its row height. You bring the visual styles via className on each part, which keeps the component flexible enough for the six shapes below: basic, default open, file tree, settings panel, controlled, and the chevron-rotation pattern.

The component supports controlled mode (open + onOpenChange props), uncontrolled mode (defaultOpen prop), and forwards all other props through context to the trigger and content. The trigger uses React.useId() to generate stable aria-controls / aria-labelledby ids, so the WAI-ARIA disclosure pattern works without you wiring id and htmlFor attributes by hand. The examples below cover every shape that comes up: a basic toggle, a file tree built from nested Collapsibles, a settings panel with multiple disclosure rows, controlled state for downstream effects, and the chevron-rotation pattern via the data-state attribute.

Basic Collapsible

The minimum Drivn Collapsible is three tags — root, trigger, content. The root holds the state and provides context. The trigger is a <button> that toggles the state on click. The content is a grid container that animates its single row from 0fr to 1fr when open. Click the trigger and the content slides open over 200ms thanks to the transition-[grid-template-rows] duration-200 styles inside the Collapsible source.

Nothing in the rendered output assumes a layout, so wrap the whole thing in any container — flex row, grid cell, sidebar item — and the component fits. The only built-in styles are the grid container and the overflow-hidden on the inner row that hides the content when the row collapses to zero height. Bring your own padding, border, and background via className on each part.

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.