Skip to content
Drivn
5 min read

How to Add a Collapsible to a React App

Step-by-step guide to adding a copy-and-own Collapsible to any React app with the Drivn CLI — smooth height animation, no measuring, fully accessible.

A collapsible is a single expand-and-collapse region — a trigger that toggles a panel of content open or shut. It sounds trivial until you try to animate the height: the content size is dynamic, so the usual max-height trick either clips tall content or animates against a guessed ceiling that looks wrong. Most libraries solve this by measuring the panel with JavaScript on every toggle. Drivn solves it with CSS grid instead, so there is nothing to measure.

The CLI copies a collapsible.tsx file into your repository. It is a compound component built with React.createContextCollapsible is the root, and Collapsible.Trigger and Collapsible.Content hang off it with dot notation. The root holds the open state and supports both uncontrolled (defaultOpen) and controlled (open + onOpenChange) modes, and it generates linked ids with React.useId() so the trigger and panel are wired together for assistive technology. Because it tracks state with useState, the source is marked 'use client', while the page around it can still render on the server.

This guide adds the Collapsible to an existing React app — install the CLI, render a basic uncontrolled toggle, drive it with controlled state, then understand the grid animation that makes it smooth without measuring. It works the same in Vite + React or a Next.js App Router project. For the full reference see the Collapsible docs; for live patterns see the Collapsible examples.

Prerequisites

Before installing the Collapsible, confirm your React project has the three things Drivn assumes: Tailwind CSS v4 installed and processing your CSS, TypeScript configured (the component ships as a .tsx file), and a @/ path alias pointing at your source directory. If you scaffolded with create-next-app, npm create vite, or npx drivn@latest create, all three are already wired. The Collapsible is one of Drivn's lightest components — it imports nothing beyond React and the cn class-merge helper, so it adds no runtime dependency at all. The installation page lists the minimal config if you are wiring a custom setup by hand.

Step 1 — Install Drivn via the CLI

Run the CLI from your project root to add the Collapsible source file. The command prompts once for your install directory (defaulting to src/components/ui/), writes collapsible.tsx, and resolves the cn utility it depends on. No global config file is created — the Collapsible is a single TypeScript file in your repo that you edit like any other component. Unlike the Combobox or Command, it pulls in no icon library and no cmdk, so the install is just the one file plus the shared cn helper. Confirm the file landed in your editor, then commit it to keep a clean baseline. The CLI docs cover the flags for targeting a custom location during install.

1# add the Collapsible to your existing React project
2npx drivn add collapsible
3
4# verify the file was written
5ls src/components/ui/collapsible.tsx

Step 2 — Render an uncontrolled collapsible

Import the Collapsible and compose it from Collapsible.Trigger and Collapsible.Content. In uncontrolled mode you do not manage any state — pass defaultOpen if you want it to start expanded, and the component tracks the rest internally. The Trigger is a real <button> that spreads any HTML button attributes you pass and toggles the panel on click; the Content holds whatever you want to reveal. The root sets a data-state of open or closed on its wrapper, so you can target either state with Tailwind's arbitrary variants for rotating a chevron or shifting a background. Because the source is 'use client', render it inside a client component in a Next.js App Router app. See the Collapsible docs for the full prop table.

1'use client'
2import { Collapsible } from '@/components/ui/collapsible'
3
4export function Faq() {
5 return (
6 <Collapsible defaultOpen>
7 <Collapsible.Trigger className="font-medium">
8 Does Drivn measure the panel height?
9 </Collapsible.Trigger>
10 <Collapsible.Content>
11 <p className="pt-2 text-muted-foreground">
12 No it animates with a CSS grid track, so there is
13 nothing to measure.
14 </p>
15 </Collapsible.Content>
16 </Collapsible>
17 )
18}

Step 3 — Drive it with controlled state

To control the open state from your own code — syncing several panels, persisting the state, or opening it in response to another action — pass open and onOpenChange. The root reads const open = controlledOpen ?? internalOpen, so providing open switches it into controlled mode and your value wins; onOpenChange fires with the next boolean every time the trigger is clicked. Hold the value in useState and update it from the callback. This is the same controlled pattern Drivn uses across its stateful components, so it composes cleanly with the Accordion when you need several linked sections. The Trigger also exposes aria-expanded and aria-controls automatically, so a controlled toggle stays accessible without extra props.

1'use client'
2import { useState } from 'react'
3import { Collapsible } from '@/components/ui/collapsible'
4
5export function Panel() {
6 const [open, setOpen] = useState(false)
7
8 return (
9 <Collapsible open={open} onOpenChange={setOpen}>
10 <Collapsible.Trigger>
11 {open ? 'Hide details' : 'Show details'}
12 </Collapsible.Trigger>
13 <Collapsible.Content>
14 <div className="pt-2">Controlled content here.</div>
15 </Collapsible.Content>
16 </Collapsible>
17 )
18}

Step 4 — Understand the grid animation

The smooth open-and-close comes from one technique, and it is worth understanding so you can restyle it safely. The Content wrapper carries the panel style — grid transition-[grid-template-rows] duration-200 — and an inline style that sets gridTemplateRows to 1fr when open and 0fr when closed. A grid row animating from 0fr to 1fr collapses and expands to fit its content with no measured pixel height, which is why tall content never clips. The inner div uses overflow-hidden so the content is masked while the row shrinks. To restyle, open src/components/ui/collapsible.tsx and edit the styles object or the duration-200 token; the trigger has no styles of its own, so you style it with the className you pass. The two-key styles object is below, verbatim from the source.

1// styles object, verbatim from src/components/ui/collapsible.tsx
2const styles = {
3 panel: 'grid transition-[grid-template-rows] duration-200',
4 content: 'overflow-hidden',
5}
Get started

Install Drivn in one command

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

Follow Drivn updates
New components, improvements, and guides every release.
Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

Frequently asked questions

Yes. Set the project up with TypeScript and Tailwind v4 first, then run npx drivn add collapsible. The Collapsible has no dependency on Next.js or any router — it renders anywhere React and Tailwind reach the DOM. It also pulls in no icon library or external primitive, so the install is just the single component file plus the shared cn helper. Vite + React is the most common non-Next setup and works without changes.

The Content is a CSS grid whose single row animates between 0fr and 1fr via inline gridTemplateRows, with transition-[grid-template-rows] duration-200 driving the tween. A 1fr row sizes itself to the content automatically, so the browser handles the height with no JavaScript measurement. An inner overflow-hidden div masks the content as the row shrinks, giving a clean collapse for content of any height.

Leave open unset for uncontrolled mode and optionally pass defaultOpen to set the starting state — the component tracks the rest internally. To control it, pass both open and onOpenChange: the root reads controlledOpen ?? internalOpen, so a defined open takes over and onOpenChange fires with the next boolean on each toggle. Hold the value in useState and update it from the callback.

Yes. The Trigger renders a real <button> with aria-expanded reflecting the open state and aria-controls pointing at the panel, while the Content is a role="region" labelled by the trigger through aria-labelledby. The linking ids are generated with React.useId(), so multiple collapsibles on a page never collide. Keyboard users get button semantics for free — focus, Enter, and Space all work without extra wiring.