Drivn vs shadcn/ui — Drawer Component Compared
Side-by-side comparison of Drivn Drawer vs shadcn/ui — native <dialog> element with four sides and zero runtime deps versus the Vaul bottom-sheet wrapper.
Drivn and shadcn/ui ship slide-in panels from different foundations. shadcn/ui's Drawer is a thin wrapper around Vaul — Emil Kowalski's drag-to-dismiss bottom-sheet library — and the published recipe re-exports Drawer, DrawerTrigger, DrawerPortal, DrawerOverlay, DrawerContent, DrawerHeader, DrawerFooter, DrawerTitle, DrawerDescription, and DrawerClose from vaul. The drag gesture, snap points, momentum, and inert background handling all live inside the Vaul runtime. Drivn's Drawer is built on the native HTML <dialog> element. The browser handles modal stacking, focus trap, and Escape via showModal(); the component file is around a hundred and fifty lines and ships zero runtime UI dependencies.
The API surface also diverges. shadcn exposes ten named exports you import flat, and a bottom sheet is the default shape — opting into left or right requires the direction prop on the root and a different content shape because Vaul's transform values follow gesture math. Drivn collapses the surface into dot notation: Drawer, Drawer.Trigger, Drawer.Content, Drawer.Close, Drawer.Header, Drawer.Footer. The side prop on Drawer.Content accepts left, right, top, or bottom, and each side gets its own translate utility in a single styles.sides map. Two imports versus ten, and the same component covers all four edges.
This page lays out every difference: underlying primitive, dependency footprint, drag-to-dismiss versus click-to-dismiss, animation technique, controlled state, and which use cases each one fits. The shadcn equivalent shown in each section matches the published drawer.tsx recipe, not a hypothetical wrapper. If you reach for Vaul specifically because you want drag-to-dismiss on a mobile bottom sheet, that is a real reason to pick shadcn — Drivn's Drawer is closer to a side-anchored Dialog.
Side-by-side comparison
| Feature | Drivn | shadcn/ui |
|---|---|---|
| Underlying primitive | Native <dialog> element | Vaul (drag-to-dismiss library) |
| Runtime UI dependencies | Zero | vaul |
| Subcomponents to import | 6 (Drawer, .Trigger, .Content, .Close, .Header, .Footer) | 10 flat exports |
| Default side | right | bottom |
| Available sides | left, right, top, bottom (one component) | bottom, top, left, right via direction prop |
| Drag-to-dismiss | ||
| Title surface | title + description props on Drawer.Header | <DrawerTitle> + <DrawerDescription> components |
| Focus trap | Browser via showModal() | Vaul focus management |
| Outside-click dismiss | Built-in | Built-in (Vaul) |
| Escape dismiss | Native cancel event | Vaul keydown handler |
| Animation | CSS transitions on translate + starting: | Vaul transform + tailwindcss-animate |
| Controlled API | open + onOpenChange | open + onOpenChange |
| License | MIT | MIT |
| Copy-paste install |
API side-by-side
shadcn/ui's Drawer is a flat namespace of ten named exports — Drawer, DrawerTrigger, DrawerPortal, DrawerOverlay, DrawerContent, DrawerHeader, DrawerFooter, DrawerTitle, DrawerDescription, DrawerClose — and the title row is built by composing <DrawerHeader> around <DrawerTitle> and <DrawerDescription>. Every drawer repeats that scaffold. Drivn collapses the surface into dot notation. Drawer.Trigger opens, Drawer.Content accepts a side prop, and Drawer.Header takes title and description as string props instead of nested components.
The call site goes from ten imports and four nested wrappers to two imports and a flat children block. The full Drivn surface, including the controlled-state hook into Drawer, stays one file you can edit after install. Need a chromeless drawer? Drop Drawer.Header and Drawer.Footer and render plain children inside Drawer.Content.
1 // shadcn/ui — flat exports + composed header 2 import { 3 Drawer, 4 DrawerContent, 5 DrawerDescription, 6 DrawerFooter, 7 DrawerHeader, 8 DrawerTitle, 9 DrawerTrigger, 10 } from '@/components/ui/drawer' 11 import { Button } from '@/components/ui/button' 12 13 export function EditDrawer() { 14 return ( 15 <Drawer> 16 <DrawerTrigger asChild> 17 <Button>Edit profile</Button> 18 </DrawerTrigger> 19 <DrawerContent> 20 <DrawerHeader> 21 <DrawerTitle>Edit profile</DrawerTitle> 22 <DrawerDescription> 23 Make changes to your profile. 24 </DrawerDescription> 25 </DrawerHeader> 26 <DrawerFooter> 27 <Button>Save</Button> 28 </DrawerFooter> 29 </DrawerContent> 30 </Drawer> 31 ) 32 } 33 34 // Drivn — dot notation + side prop + string props on Header 35 import { Drawer } from '@/components/ui/drawer' 36 import { Button } from '@/components/ui/button' 37 38 export default function Page() { 39 return ( 40 <Drawer> 41 <Drawer.Trigger>Edit profile</Drawer.Trigger> 42 <Drawer.Content side="right"> 43 <Drawer.Close /> 44 <Drawer.Header 45 title="Edit profile" 46 description="Make changes to your profile." 47 /> 48 <Drawer.Footer> 49 <Button>Save</Button> 50 </Drawer.Footer> 51 </Drawer.Content> 52 </Drawer> 53 ) 54 }
Dependency footprint
shadcn's Drawer adds vaul to your package.json. Vaul is a focused library — about 30 KB minified — and it handles the parts that make bottom sheets feel native: drag-to-dismiss, snap points, momentum, and body scaling on iOS. The shadcn wrapper file mostly re-exports Vaul's primitives and styles the content with the same bg-background and rounded-top utilities you see across the shadcn recipes.
Drivn ships zero runtime UI dependencies. The Drawer source imports React, lucide-react for the close icon, cn from your local utils, and Drivn's Button for the trigger — no Vaul, no animate utility plugin. The native <dialog> element provides modal stacking, focus trap, and Escape via showModal(). The full component is one file you own, and bundle analysis shows the drawer adding under 2 KB of gzipped JS to your route. The tradeoff is real: no drag gesture, no snap points. If those are critical to your UX, Vaul is the right pick.
1 // shadcn/ui — package.json after `npx shadcn add drawer` 2 { 3 "dependencies": { 4 "vaul": "^1.x" 5 } 6 } 7 8 // Drivn — package.json after `npx drivn add drawer` 9 // (no new dependencies added)
Animation technique
shadcn's Drawer animates through Vaul. The library reads pointer events, runs transform math during the drag, and snaps to a closed or open position with a spring-style easing when the gesture ends. The visual result is a bottom sheet that follows your finger, and the timing is tuned for mobile.
Drivn animates with plain CSS transitions on translate, using Tailwind 4's starting: and group-open: variants. Each side gets its own translate utility: translate-x-[100%] group-open:translate-x-[0%] for the right side, mirrored for left, and the equivalent on the y axis for top and bottom. The overlay fades via open:bg-overlay open:backdrop-blur-sm on the same <dialog> element. The transition is 300 ms, ease-out, and CSS-only — no JavaScript timer drives the animation. Copy the styles.sides block from drawer.ts into another component and the same slide-in pattern applies.
1 // Drivn — animation lives in styles.sides on the registry component 2 const styles = { 3 overlay: cn( 4 'group fixed inset-0 m-0 p-0 border-none outline-none', 5 'max-w-none max-h-none w-screen h-dvh overflow-clip', 6 'bg-transparent backdrop-blur-none', 7 'open:bg-overlay open:backdrop-blur-sm', 8 'starting:open:bg-transparent starting:open:backdrop-blur-none', 9 'transition-[display,overlay,background-color,backdrop-filter]', 10 'duration-300 ease-out transition-discrete' 11 ), 12 content: cn( 13 'fixed flex flex-col', 14 'bg-card shadow-xl', 15 'transition-[translate,display,overlay] duration-300', 16 'ease-out transition-discrete' 17 ), 18 sides: { 19 right: cn( 20 'right-0 top-0 h-dvh w-full max-w-[400px]', 21 'border-l border-border', 22 'translate-x-[100%] group-open:translate-x-[0%]', 23 'starting:group-open:translate-x-[100%]' 24 ), 25 left: cn( 26 'left-0 top-0 h-dvh w-full max-w-[400px]', 27 'border-r border-border', 28 'translate-x-[-100%] group-open:translate-x-[0%]', 29 'starting:group-open:translate-x-[-100%]' 30 ), 31 }, 32 }
Focus, Escape, and outside-click dismiss
shadcn delegates focus trap, Escape, and outside-click handling to Vaul. Vaul covers the edge cases — return focus on close, layered drawers, pointer-down outside detection, body inert on open — at the cost of a runtime dependency tuned for mobile gesture handling.
Drivn leans on the native <dialog> element for the same behaviors. el.showModal() enters the top-layer, traps focus inside, and dispatches a cancel event when Escape is pressed — the component listens for that event and calls setOpen(false) after preventDefault(). Outside-click is a single click handler on the <dialog> that closes when e.target === e.currentTarget. Body overflow is locked to hidden while the drawer is open and restored on close. No portal, no synthetic keydown listener. The behavior is identical to the Dialog component because they share the same primitive.
1 React.useEffect(() => { 2 const el = ref.current 3 if (!el) return 4 const onCancel = (e: Event) => { 5 e.preventDefault() 6 setOpen(false) 7 } 8 el.addEventListener('cancel', onCancel) 9 if (open) { 10 if (!el.open) el.showModal() 11 document.body.style.overflow = 'hidden' 12 } else if (el.open) { 13 el.close() 14 document.body.style.overflow = '' 15 } 16 return () => el.removeEventListener('cancel', onCancel) 17 }, [open, setOpen]) 18 19 // outside-click on the <dialog> backdrop 20 onClick={(e) => { 21 if (e.target === e.currentTarget) setOpen(false) 22 }}
Use cases — when each wins
Pick shadcn's Vaul-backed Drawer when the drawer is a mobile bottom sheet and the drag-to-dismiss gesture is part of the experience. iOS-style sheets that snap to half-height or full-height, photo viewers, and contextual menus on touch devices all benefit from the gesture model Vaul provides. The runtime cost is the price of admission for that feel.
Pick Drivn's Drawer when you want a side-anchored slide-in for desktop or mobile that behaves like a Dialog — settings panels, edit forms, navigation drawers, a notifications tray. Drivn gives you all four sides in one component with one side prop, the API matches every other Drivn compound, and the bundle stays under 2 KB. If you later add a mobile bottom sheet that needs drag gestures, drop in Vaul for that specific surface — the two patterns can coexist without conflict.
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 — Drivn Drawer is a side-anchored slide-in, not a gesture-driven bottom sheet. It closes on outside-click, Escape, or the built-in close button. If drag-to-dismiss is critical to your UX, Vaul (the library shadcn wraps) is the right pick for that surface. The two patterns can coexist in the same app — use Drivn Drawer for settings panels and navigation, drop in Vaul for a mobile bottom sheet that needs the gesture.
The native <dialog> element provides top-layer stacking, focus trap, and Escape-via-cancel-event without a runtime dependency. Drivn reuses the same primitive across Dialog and Drawer, which keeps the lifecycle and controlled API identical between the two components. Browser support is universal across evergreen targets, the shipped file stays around a hundred and fifty lines, and animation is plain CSS transitions on translate.
Yes — pass side="left", side="top", or side="bottom" to Drawer.Content. The styles.sides map in the registry source defines one translate utility per side, so all four edges work out of the box with the same component. The default is right. Each side picks up the correct border (left, right, top, or bottom) and a sensible default width or height — 400 px max-width for side drawers, 400 px height for top and bottom.
Most of it, yes. The controlled API matches — open plus onOpenChange on the root — so any state machine driving a shadcn drawer drives a Drivn drawer as-is. The structural changes happen at the JSX layer: collapse <DrawerHeader><DrawerTitle>...</DrawerTitle><DrawerDescription>...</DrawerDescription></DrawerHeader> into <Drawer.Header title="..." description="..." />, and add the side prop on Drawer.Content if you want left, right, or top instead of the shadcn default of bottom.
The Drawer file starts with "use client" because it manages state with useState and listens to DOM events on the <dialog> element. You can import it from a Server Component, and Next.js draws the client boundary automatically based on the directive at the top of the file. Render the trigger and content inside a server-rendered page — the drawer hydrates on the client without extra configuration in your layout or page.

