Drivn vs shadcn/ui — Collapsible Component Compared
Drivn vs shadcn/ui React Collapsible: Drivn animates with grid-template-rows and ships zero deps — shadcn wraps @radix-ui/react-collapsible with keyframes.
Drivn and shadcn/ui both ship a Collapsible compound component, but the runtime profile is different. shadcn wraps @radix-ui/react-collapsible, exposes three top-level exports (Collapsible, CollapsibleTrigger, CollapsibleContent), and animates the panel via CSS keyframes that key off Radix' data-state="open" and data-state="closed" attributes. Drivn ships a single compound primitive with dot notation — Collapsible, Collapsible.Trigger, Collapsible.Content — animates the panel via the modern grid-template-rows: 0fr → 1fr CSS transition, and pulls in zero runtime dependencies beyond the local cn utility.
The API surface diverges in two places: the import shape and the animation technique. Drivn uses one import and dot notation: import { Collapsible } from "@/components/ui/collapsible" then <Collapsible.Trigger> and <Collapsible.Content>. shadcn requires three named imports from a single module path. The behavioral difference shows up in animation. Drivn animates the height by transitioning the parent grid container's grid-template-rows from 0fr to 1fr while the inner row uses overflow-hidden — no measurement, no max-height hack, no keyframe definitions. shadcn relies on Radix' data-state attribute and Tailwind keyframes you copy from the docs (accordion-down, accordion-up) into your tailwind.config.
This page walks through every surface where the two diverge: the dependency tree, the dot-notation API, the grid-rows animation, and the ARIA wiring. Every snippet below compiles against the Collapsible source the Drivn CLI writes into your repo. If you have shadcn's Collapsible today, the migration is removing the @radix-ui/react-collapsible dependency, deleting the keyframe definitions from tailwind.config, and swapping the three named imports for <Collapsible.Trigger> and <Collapsible.Content> under one root.
Side-by-side comparison
| Feature | Drivn | shadcn/ui |
|---|---|---|
| Underlying primitive | Native button + grid-rows transition | @radix-ui/react-collapsible |
| Compound API | Dot notation (Collapsible.Trigger) | Three named exports |
| Imports per use | One named import | Three named imports |
| Animation technique | grid-template-rows 0fr → 1fr | CSS keyframes via data-state |
| Tailwind keyframes required | ||
| ARIA wiring | useId-generated triggerId/contentId | Radix internal |
| Controlled API | open + onOpenChange | open + onOpenChange |
| Runtime UI deps | Zero | @radix-ui/react-collapsible |
| License | MIT | MIT |
| Copy-paste install |
API side-by-side
shadcn's Collapsible exports three top-level components from one module — Collapsible, CollapsibleTrigger, CollapsibleContent. You import all three at the top of every file that uses one. Drivn ships a single export and dot notation. The Collapsible source defines Trigger and Content inside the file, then the last line attaches them via Object.assign(CollapsibleRoot, { Trigger, Content }). The result is one import name, full TypeScript autocomplete on the children, and a JSX tree where every part of the compound shares a visible parent.
The shared state — open, toggle, triggerId, contentId — flows between the parts via a React Context the root provider mounts. shadcn does the same job through Radix' internal context. Both are correct, but the Drivn version lives in the file you can read in your editor without opening node_modules. When the trigger fires toggle, the context updates, both the trigger's aria-expanded and the content's data-state re-render, and the grid-rows transition kicks in.
1 // shadcn/ui — three exports, one module path 2 import { 3 Collapsible, 4 CollapsibleTrigger, 5 CollapsibleContent, 6 } from '@/components/ui/collapsible' 7 8 <Collapsible> 9 <CollapsibleTrigger>Toggle</CollapsibleTrigger> 10 <CollapsibleContent>Hidden content</CollapsibleContent> 11 </Collapsible> 12 13 // Drivn — one import, dot notation 14 import { Collapsible } from '@/components/ui/collapsible' 15 16 <Collapsible> 17 <Collapsible.Trigger>Toggle</Collapsible.Trigger> 18 <Collapsible.Content>Hidden content</Collapsible.Content> 19 </Collapsible>
The grid-template-rows animation
shadcn animates the panel by binding CSS keyframes to Radix' data-state="open" and data-state="closed" attributes. You copy two keyframe definitions (accordion-down, accordion-up) into tailwind.config, register two animation utilities (animate-collapsible-down, animate-collapsible-up), and apply them via data-[state=open]:animate-collapsible-down. The keyframes animate height from 0 to var(--radix-collapsible-content-height), a CSS custom property Radix sets via JavaScript measurement on every render.
Drivn uses the modern grid-template-rows transition technique. The Collapsible source wraps the content in a grid container with grid-template-rows set to 0fr when closed and 1fr when open, and the inner row carries overflow-hidden. Browsers interpolate fr units smoothly, so the height transitions over the 200ms duration-200 without measurement, without keyframes, without a custom property, and without JavaScript reading the DOM. Two CSS rules and one inline style prop replace the entire keyframe pipeline.
1 // Drivn rendered DOM — verbatim from the Collapsible source 2 const styles = { 3 panel: 'grid transition-[grid-template-rows] duration-200', 4 content: 'overflow-hidden', 5 } 6 7 // Inside Collapsible.Content 8 <div 9 id={contentId} 10 role="region" 11 aria-labelledby={triggerId} 12 data-state={open ? 'open' : 'closed'} 13 className={styles.panel} 14 style={{ gridTemplateRows: open ? '1fr' : '0fr' }} 15 > 16 <div className={cn(styles.content, className)}> 17 {children} 18 </div> 19 </div>
ARIA wiring without Radix internals
Both libraries follow the WAI-ARIA disclosure pattern — the trigger reports aria-expanded and aria-controls, the content reports role="region" and aria-labelledby. shadcn delegates the id generation and attribute synchronization to Radix. Drivn does it inline with React.useId(). The root component generates a stable id, derives triggerId and contentId from the generated id, and pushes both through context. The trigger reads triggerId for its own id and contentId for aria-controls. The content reads contentId for its own id and triggerId for aria-labelledby.
The payoff is that everything you read with a screen-reader inspector or in the browser's accessibility panel maps directly to a line in your component file. Debugging an ARIA wiring issue does not require opening node_modules/@radix-ui/react-collapsible/dist/index.mjs — the Collapsible source shows the exact strings being attached. The same useId pattern is used by Dialog, Dropdown, and Tooltip, so once you have read one Drivn ARIA implementation you have read them all.
1 // Drivn ARIA wiring — verbatim from the source 2 const id = React.useId() 3 const triggerId = `${id}trigger` 4 const contentId = `${id}content` 5 6 // Trigger 7 <button 8 id={triggerId} 9 aria-expanded={open} 10 aria-controls={contentId} 11 data-state={open ? 'open' : 'closed'} 12 onClick={toggle} 13 > 14 {children} 15 </button> 16 17 // Content 18 <div 19 id={contentId} 20 role="region" 21 aria-labelledby={triggerId} 22 data-state={open ? 'open' : 'closed'} 23 > 24 {children} 25 </div>
Controlled state and chevron rotation
Both libraries support controlled and uncontrolled modes via the same prop names — open, onOpenChange, defaultOpen. The Drivn root detects the mode with const open = controlledOpen ?? internalOpen and skips its internal setState call when controlled — the same pattern used in shadcn. The migration cost between the two is zero on this surface.
The one place the Drivn API helps with downstream styling is the data-state attribute on the root. The root sets data-state={open ? 'open' : 'closed'} on the outer <div>, so any descendant can target the open state via Tailwind's group-data-[state=open]/<name> variant. A common pattern is rotating a chevron icon in the trigger when the panel opens — set className="group/coll" on the Collapsible root and className="transition-transform group-data-[state=open]/coll:rotate-180" on the chevron, and the icon flips without any extra state. The same trick works for any number of nested collapsibles thanks to Tailwind's named groups.
1 'use client' 2 import { useState } from 'react' 3 import { Collapsible } from '@/components/ui/collapsible' 4 import { ChevronDown } from 'lucide-react' 5 6 export function Settings() { 7 const [open, setOpen] = useState(false) 8 9 return ( 10 <Collapsible 11 open={open} 12 onOpenChange={setOpen} 13 className="group/coll" 14 > 15 <Collapsible.Trigger className="flex items-center gap-2"> 16 Advanced settings 17 <ChevronDown className="w-4 h-4 transition-transform group-data-[state=open]/coll:rotate-180" /> 18 </Collapsible.Trigger> 19 <Collapsible.Content> 20 Hidden settings panel 21 </Collapsible.Content> 22 </Collapsible> 23 ) 24 }
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
Frequently asked questions
No. The Collapsible imports React for useState, useId, useContext, and createContext, plus cn from the local @/utils/cn utility — that is it. There is no @radix-ui/react-collapsible, no cva, no class-variance-authority, no clsx wrapper, no Tailwind keyframe configuration. The animation runs on the modern grid-template-rows: 0fr → 1fr CSS transition that browsers interpolate natively.
Browsers can interpolate fr units in grid-template-rows smoothly, so a parent grid container transitioning from grid-template-rows: 0fr to grid-template-rows: 1fr animates the row's rendered height from zero to its natural size over the transition duration. The Drivn Collapsible source sets transition-[grid-template-rows] duration-200 on the panel and toggles the inline style between 0fr and 1fr — no JavaScript measurement, no CSS custom properties, no keyframes.
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, then put className="transition-transform group-data-[state=open]/coll:rotate-180" on the chevron icon inside the trigger. The icon rotates 180 degrees when the panel opens. Use named groups (group/coll, group/notif, group/privacy) to keep multiple nested Collapsibles independent.
Three steps. First, run npx drivn add collapsible to write the Drivn Collapsible source into your repo. Second, replace import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible' with import { Collapsible } from '@/components/ui/collapsible' and rewrite the JSX tags as <Collapsible.Trigger> and <Collapsible.Content>. Third, uninstall @radix-ui/react-collapsible and remove the accordion-down and accordion-up keyframes from your tailwind.config — the grid-rows transition replaces them.