Vaul
Drawer component for React by Emil Kowalski — bottom sheets, snap points, nested drawers, clean API
SKILL.mdskills/vaul/SKILL.md
GitHubVaul is a drawer (bottom sheet) component for React by Emil Kowalski. Use it for mobile-style slide-up panels, settings sheets, and contextual actions.
Installation
npm install vaul
# or
pnpm i vaul
Basic Usage
'use client';
import { Drawer } from 'vaul';
export function MyDrawer() {
return (
<Drawer.Root>
<Drawer.Trigger asChild>
<button>Open</button>
</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 bg-black/40" />
<Drawer.Content className="bg-white flex flex-col rounded-t-[10px] h-full mt-24 fixed bottom-0 left-0 right-0">
<div className="p-4 bg-white rounded-t-[10px] flex-1">
<div className="mx-auto w-12 h-1.5 flex-shrink-0 rounded-full bg-gray-300 mb-8" />
<h2 className="text-lg font-semibold">Title</h2>
<p>Content goes here.</p>
</div>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
);
}
Components API
| Component | Description |
|---|---|
Drawer.Root |
Root state manager |
Drawer.Trigger |
Opens the drawer |
Drawer.Portal |
Renders outside DOM tree |
Drawer.Overlay |
Backdrop |
Drawer.Content |
The drawer panel |
Drawer.Title |
Accessible title |
Drawer.Description |
Accessible description |
Drawer.Close |
Close trigger |
Drawer.Root Props
| Prop | Type | Default | Description |
|---|---|---|---|
open |
boolean | — | Controlled open state |
onOpenChange |
function | — | State change callback |
defaultOpen |
boolean | false | Uncontrolled default |
snapPoints |
(string|number)[] | — | Snap positions |
activeSnapPoint |
string|number | — | Controlled snap |
setActiveSnapPoint |
function | — | Snap change callback |
direction |
string | bottom |
top right left bottom |
dismissible |
boolean | true | Allow drag-to-close |
shouldScaleBackground |
boolean | false | Scale page behind drawer |
modal |
boolean | true | Block background interaction |
onClose |
function | — | On close callback |
fadeFromIndex |
number | — | Snap fade index |
Snap Points
<Drawer.Root snapPoints={['148px', '355px', 1]} fadeFromIndex={2}>
{/* 1 = 100% height */}
</Drawer.Root>
Controlled State
const [open, setOpen] = React.useState(false);
<Drawer.Root open={open} onOpenChange={setOpen}>
<Drawer.Trigger onClick={() => setOpen(true)}>Open</Drawer.Trigger>
...
<Drawer.Close onClick={() => setOpen(false)}>Close</Drawer.Close>
</Drawer.Root>
Direction Variants
// Side drawer (right panel)
<Drawer.Root direction="right">
<Drawer.Content className="fixed right-0 top-0 h-full w-[400px]">
...
</Drawer.Content>
</Drawer.Root>
Scale Background (iOS-style)
// Add to body wrapper
<div vaul-drawer-wrapper="">
<App />
</div>
// Enable on drawer
<Drawer.Root shouldScaleBackground>
Nested Drawers
<Drawer.Root>
<Drawer.Trigger>Open</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Content>
<Drawer.Root nested>
<Drawer.Trigger>Open nested</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Content>Nested content</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
Clean Code Rules
- Always add
<div className="mx-auto w-12 h-1.5 rounded-full bg-gray-300 mb-8" />drag handle at top - Use
shouldScaleBackgroundfor immersive mobile-like feel (requiresvaul-drawer-wrapperon body) - Use
direction="right"for settings/filter panels,direction="bottom"for actions - Combine with Sonner for toast feedback on drawer actions
- Keep drawer content scrollable: use
overflow-y-autoon the inner content div