Base Components
Drawer
A panel that slides in from the edge of the screen. Built with Vaul; supports top, bottom, left, and right directions with drag-to-dismiss.
Example
The demo shows an app-style drawer fixed to the right (300px wide) with TopBar (app branding variants) and ContextView (full, with actions, with link, or minimal). Pick top bar and context view variants, then click Open drawer; use the close icon or click outside to dismiss.Installation
Install the following dependencies:
npm install vaulCreate a drawer.tsx file and paste the following code into it.
"use client";import * as React from "react";import { Drawer as DrawerPrimitive } from "vaul";import { cn } from "@/lib/utils";import { ContextView } from "@/components/ui/context-view";import { TopBar, TopBarLeft, TopBarRight } from "@/components/ui/top-bar";function Drawer({ ...props}: React.ComponentProps<typeof DrawerPrimitive.Root>) { return <DrawerPrimitive.Root data-slot="drawer" {...props} />;}function DrawerTrigger({ ...props}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) { return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;}function DrawerPortal({ ...props}: React.ComponentProps<typeof DrawerPrimitive.Portal>) { return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;}function DrawerClose({ ...props}: React.ComponentProps<typeof DrawerPrimitive.Close>) { return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;}function DrawerOverlay({ className, ...props}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) { return ( <DrawerPrimitive.Overlay data-slot="drawer-overlay" className={cn( "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", className, )} {...props} /> );}function DrawerContent({ className, children, ...props}: React.ComponentProps<typeof DrawerPrimitive.Content>) { return ( <DrawerPortal data-slot="drawer-portal"> <DrawerOverlay /> <DrawerPrimitive.Content data-slot="drawer-content" className={cn( "group/drawer-content bg-code-background fixed z-50 flex h-full flex-col overflow-hidden", "data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b", "data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t", "data-[vaul-drawer-direction=right]:top-6 data-[vaul-drawer-direction=right]:bottom-6 data-[vaul-drawer-direction=right]:right-6 data-[vaul-drawer-direction=right]:h-auto data-[vaul-drawer-direction=right]:min-w-[340px] data-[vaul-drawer-direction=right]:max-w-[calc(100vw-3rem)] data-[vaul-drawer-direction=right]:rounded-lg data-[vaul-drawer-direction=right]:shadow-[-4px_0_24px_rgba(0,0,0,0.08)]", "data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm", className, )} {...props} > <div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" /> {children} </DrawerPrimitive.Content> </DrawerPortal> );}function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="drawer-header" className={cn( "flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left", className, )} {...props} /> );}function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="drawer-footer" className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} /> );}function DrawerTitle({ className, ...props}: React.ComponentProps<typeof DrawerPrimitive.Title>) { return ( <DrawerPrimitive.Title data-slot="drawer-title" className={cn("text-foreground font-semibold", className)} {...props} /> );}function DrawerDescription({ className, ...props}: React.ComponentProps<typeof DrawerPrimitive.Description>) { return ( <DrawerPrimitive.Description data-slot="drawer-description" className={cn("text-muted-foreground text-sm", className)} {...props} /> );}function DrawerBar({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="drawer-bar" className={cn( "flex flex-none items-center justify-between gap-2 border-b border-border bg-background px-4 py-3", className, )} {...props} /> );}function DrawerTitleBlock({ className, ...props}: React.ComponentProps<"div">) { return ( <div data-slot="drawer-title-block" className={cn("flex flex-col gap-1 px-4 py-4", className)} {...props} /> );}function DrawerBody({ className, ...props }: React.ComponentProps<"div">) { return ( <div data-slot="drawer-body" className={cn( "flex min-h-0 flex-1 flex-col overflow-auto border-t border-border px-4 py-4", className, )} {...props} /> );}export { Drawer, DrawerPortal, DrawerOverlay, DrawerTrigger, DrawerClose, DrawerContent, DrawerHeader, DrawerFooter, DrawerTitle, DrawerDescription, DrawerBar, DrawerTitleBlock, DrawerBody, TopBar, TopBarLeft, TopBarRight, ContextView,};Check the import paths to ensure they match your project setup.
Usage
ComposeDrawer, DrawerTrigger, DrawerContent, and optional DrawerHeader, DrawerTitle, DrawerDescription, DrawerFooter, and DrawerClose. Control open state with open and onOpenChange for a controlled drawer.
import {
Drawer,
DrawerTrigger,
DrawerContent,
DrawerHeader,
DrawerTitle,
DrawerDescription,
DrawerFooter,
DrawerClose,
} from "@/components/ui/drawer";
import { Button } from "@/components/ui/button";
<Drawer>
<DrawerTrigger asChild>
<Button variant="outline">Open</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Title</DrawerTitle>
<DrawerDescription>Description</DrawerDescription>
</DrawerHeader>
<div className="p-4">Content</div>
<DrawerFooter>
<DrawerClose asChild>
<Button variant="outline">Cancel</Button>
</DrawerClose>
<Button>Submit</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>Direction and app-drawer layout
Setdirection on the root (e.g. "right" for a 300px right-side panel with shadow and rounded left edge). Use TopBar (with variant) for the header bar; TopBarLeft and TopBarRight for icon + name and menu + close. Use ContextView (with variant) for the block below: title, description, optional link, and optional primary/secondary actions. ContextView variants: full (link + buttons), withActions (buttons only), withLink (link only), minimal (title + description). Then DrawerBody for the main scrollable content.
<Drawer direction="right">
<DrawerTrigger asChild><Button>Open drawer</Button></DrawerTrigger>
<DrawerContent>
<TopBar variant="default">
<TopBarLeft><Icon /><span>App Name</span></TopBarLeft>
<TopBarRight>
<Button size="icon">⋯</Button>
<DrawerClose asChild><Button size="icon">×</Button></DrawerClose>
</TopBarRight>
</TopBar>
<ContextView
variant="full"
title="Drawer title"
description="Secondary information"
link={<a href="#">Optional link</a>}
primaryAction={<Button className="w-full">Primary action</Button>}
secondaryAction={<Button variant="outline" className="w-full">Secondary action</Button>}
/>
<DrawerBody>Main content</DrawerBody>
</DrawerContent>
</Drawer>Examples
App drawer with TopBar and ContextView
Right-side drawer (300px) with TopBar app-branding variants and ContextView variants (full, with actions, with link, minimal).API Reference
Drawer (root)
Number between 0 and 1 that determines when the drawer should be closed. Example: threshold of 0.5 would close the drawer if the user swiped for 50% of the height of the drawer or more.
When `true` the `body` doesn't get any styles assigned from Vaul
When `false` we don't change body's background color when the drawer is open.
Duration for which the drawer is not draggable after scrolling content inside of the drawer.
When `true`, don't move the drawer upwards if there's space, but rather only change it's height so it's fully scrollable when the keyboard is open
When `true` only allows the drawer to be dragged by the `<Drawer.Handle />` component.
When `false` dragging, clicking outside, pressing esc, etc. will not close the drawer. Use this in comination with the `open` prop, otherwise you won't be able to open/close the drawer.
When `false` it allows to interact with elements outside of the drawer without closing it.
Direction of the drawer. Can be `top` or `bottom`, `left`, `right`.
Opened by default, skips initial enter animation. Still reacts to `open` state changes
When set to `true` prevents scrolling on the document body on mount, and restores it on unmount.
When `true` Vaul will reposition inputs rather than scroll then into view if the keyboard is in the way. Setting it to `false` will fall back to the default browser behavior.
Disabled velocity based swiping for snap points. This means that a snap point won't be skipped even if the velocity is high enough. Useful if each snap point in a drawer is equally important.
Gets triggered after the open or close animation ends, it receives an `open` argument with the `open` state of the drawer by the time the function was triggered. Useful to revert any state changes for example.
| Prop | Type | Default | Description |
|---|---|---|---|
| activeSnapPoint | string | number | null | - | - |
| setActiveSnapPoint | ((snapPoint: string | number | null) => void) | - | - |
| open | boolean | - | - |
| closeThreshold | number | 0.25 | Number between 0 and 1 that determines when the drawer should be closed. Example: threshold of 0.5 would close the drawer if the user swiped for 50% of the height of the drawer or more. |
| noBodyStyles | boolean | - | When `true` the `body` doesn't get any styles assigned from Vaul |
| onOpenChange | ((open: boolean) => void) | - | - |
| shouldScaleBackground | boolean | - | - |
| setBackgroundColorOnScale | boolean | true | When `false` we don't change body's background color when the drawer is open. |
| scrollLockTimeout | number | 500ms | Duration for which the drawer is not draggable after scrolling content inside of the drawer. |
| fixed | boolean | - | When `true`, don't move the drawer upwards if there's space, but rather only change it's height so it's fully scrollable when the keyboard is open |
| handleOnly | boolean | false | When `true` only allows the drawer to be dragged by the `<Drawer.Handle />` component. |
| dismissible | boolean | true | When `false` dragging, clicking outside, pressing esc, etc. will not close the drawer. Use this in comination with the `open` prop, otherwise you won't be able to open/close the drawer. |
| onDrag | ((event: PointerEvent<HTMLDivElement>, percentageDragged: number) => void) | - | - |
| onRelease | ((event: PointerEvent<HTMLDivElement>, open: boolean) => void) | - | - |
| modal | boolean | true | When `false` it allows to interact with elements outside of the drawer without closing it. |
| nested | boolean | - | - |
| onClose | (() => void) | - | - |
| direction | "left" | "right" | "top" | "bottom" | 'bottom' | Direction of the drawer. Can be `top` or `bottom`, `left`, `right`. |
| defaultOpen | boolean | false | Opened by default, skips initial enter animation. Still reacts to `open` state changes |
| disablePreventScroll | boolean | false | When set to `true` prevents scrolling on the document body on mount, and restores it on unmount. |
| repositionInputs | boolean | true when {@link snapPoints } is defined | When `true` Vaul will reposition inputs rather than scroll then into view if the keyboard is in the way. Setting it to `false` will fall back to the default browser behavior. |
| snapToSequentialPoint | boolean | false | Disabled velocity based swiping for snap points. This means that a snap point won't be skipped even if the velocity is high enough. Useful if each snap point in a drawer is equally important. |
| container | HTMLElement | null | - | - |
| onAnimationEnd | ((open: boolean) => void) | - | Gets triggered after the open or close animation ends, it receives an `open` argument with the `open` state of the drawer by the time the function was triggered. Useful to revert any state changes for example. |
| preventScrollRestoration | boolean | - | - |
| autoFocus | boolean | - | - |
| Prop | Type | Description |
|---|---|---|
open | boolean | Controlled open state. |
onOpenChange | (open: boolean) => void | Called when open state changes. |
direction | "top" | "bottom" | "left" | "right" | Slide direction. Default: "bottom". |
DrawerTrigger, DrawerContent, DrawerClose
UseDrawerTrigger to open; wrap content in DrawerContent. Use DrawerClose inside the drawer to close (e.g. on a button with asChild).
DrawerHeader, DrawerFooter, DrawerTitle, DrawerDescription
Layout and accessibility: put title and description inDrawerHeader, actions in DrawerFooter. DrawerTitle and DrawerDescription are used for screen readers.
TopBar (app branding)
Separate component used as the drawer header. TopBar accepts avariant: "default" (light), "pink", "darkBlue", "yellow", "teal", "darkTeal". Use TopBarLeft for icon + app name and TopBarRight for menu + close. Exported from the drawer module for use inside DrawerContent.
ContextView (drawer/header context)
Section below the top bar with title, description, and optional link and action buttons. ContextView acceptsvariant: "full" (link + primary + secondary actions), "withActions" (buttons only), "withLink" (link only), "minimal" (title + description only). Props: title, description, link, primaryAction, secondaryAction. Exported from the drawer module for use inside DrawerContent.