Drawer
A panel that slides in from the edge of the screen, with support for swipe gestures to dismiss, making it mobile-friendly.
Drawer adds gesture support, snap points, and indent effects. If you don't need these, use Sheet instead.
Installation
CLI
npx shadcn@latest add https://exawizards.com/exabase/design/registry/drawer.jsonManual
Copy and paste the following code into your project.
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "@base-ui/react/drawer"
import { cn } from "@/lib/utils"
const swipeDirectionMap = {
bottom: "down",
top: "up",
right: "right",
left: "left",
} as const
function Drawer({
direction = "bottom",
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root> & {
direction?: "top" | "right" | "bottom" | "left"
}) {
const swipeDirection = swipeDirectionMap[direction]
return (
<DrawerPrimitive.Root
data-slot="drawer"
swipeDirection={swipeDirection}
{...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.Backdrop>) {
return (
<DrawerPrimitive.Backdrop
data-slot="drawer-overlay"
className={cn(
"fixed inset-0 min-h-dvh bg-black opacity-[calc(0.5*(1-var(--drawer-swipe-progress)))] transition-opacity duration-[450ms] ease-[cubic-bezier(0.32,0.72,0,1)] data-[ending-style]:opacity-0 data-[ending-style]:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-[starting-style]:opacity-0 data-[swiping]:duration-0",
className
)}
{...props}
/>
)
}
function DrawerContent({
className,
children,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
return (
<DrawerPortal data-slot="drawer-portal">
<DrawerOverlay />
<DrawerPrimitive.Viewport className="fixed inset-0 z-50">
<DrawerPrimitive.Popup
className={cn(
"group/drawer-content fixed touch-auto overflow-y-auto overscroll-contain bg-popover text-foreground outline outline-border transition-transform duration-[450ms] ease-[cubic-bezier(0.32,0.72,0,1)] data-[ending-style]:duration-[calc(var(--drawer-swipe-strength)*400ms)] data-[swiping]:select-none",
// right
"data-[swipe-direction=right]:inset-y-0 data-[swipe-direction=right]:right-0 data-[swipe-direction=right]:-mr-[3rem] data-[swipe-direction=right]:w-full data-[swipe-direction=right]:[transform:translateX(var(--drawer-swipe-movement-x))] data-[swipe-direction=right]:pr-[3rem] data-[swipe-direction=right]:data-[ending-style]:[transform:translateX(calc(100%-3rem))] data-[swipe-direction=right]:data-[starting-style]:[transform:translateX(calc(100%-3rem))] data-[swipe-direction=right]:sm:max-w-sm",
// left
"data-[swipe-direction=left]:inset-y-0 data-[swipe-direction=left]:left-0 data-[swipe-direction=left]:-ml-[3rem] data-[swipe-direction=left]:w-full data-[swipe-direction=left]:[transform:translateX(var(--drawer-swipe-movement-x))] data-[swipe-direction=left]:pl-[3rem] data-[swipe-direction=left]:data-[ending-style]:[transform:translateX(calc(-100%+3rem))] data-[swipe-direction=left]:data-[starting-style]:[transform:translateX(calc(-100%+3rem))] data-[swipe-direction=left]:sm:max-w-sm",
// top
"data-[swipe-direction=up]:inset-x-0 data-[swipe-direction=up]:top-0 data-[swipe-direction=up]:-mt-[3rem] data-[swipe-direction=up]:max-h-[80vh] data-[swipe-direction=up]:[transform:translateY(var(--drawer-swipe-movement-y))] data-[swipe-direction=up]:rounded-b-xl data-[swipe-direction=up]:pt-[calc(env(safe-area-inset-top,0px)+3rem)] data-[swipe-direction=up]:data-[ending-style]:[transform:translateY(calc(-100%+3rem))] data-[swipe-direction=up]:data-[starting-style]:[transform:translateY(calc(-100%+3rem))]",
// down
"data-[swipe-direction=down]:inset-x-0 data-[swipe-direction=down]:bottom-0 data-[swipe-direction=down]:-mb-[3rem] data-[swipe-direction=down]:max-h-[80vh] data-[swipe-direction=down]:[transform:translateY(var(--drawer-swipe-movement-y))] data-[swipe-direction=down]:rounded-t-xl data-[swipe-direction=down]:pb-[calc(env(safe-area-inset-bottom,0px)+3rem)] data-[swipe-direction=down]:data-[ending-style]:[transform:translateY(calc(100%-3rem))] data-[swipe-direction=down]:data-[starting-style]:[transform:translateY(calc(100%-3rem))]"
)}
{...props}
>
<DrawerPrimitive.Content
data-slot="drawer-content"
className={cn("flex flex-col text-sm", className)}
{...props}
>
{children}
</DrawerPrimitive.Content>
</DrawerPrimitive.Popup>
</DrawerPrimitive.Viewport>
</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-lg font-semibold", className)}
{...props}
/>
)
}
function DrawerDescription({
className,
...props
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
return (
<DrawerPrimitive.Description
data-slot="drawer-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
export {
Drawer,
DrawerPortal,
DrawerOverlay,
DrawerTrigger,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerFooter,
DrawerTitle,
DrawerDescription,
}
Usage
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"<Drawer>
<DrawerTrigger>Open</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Are you absolutely sure?</DrawerTitle>
<DrawerDescription>This action cannot be undone.</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<Button>Submit</Button>
<DrawerClose>
<Button variant="outline">Cancel</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>See the Vaul documentation for more information.
Examples
Sides
API
Drawer
Groups all parts of the drawer. Doesn't render its own HTML element.
| Prop | Type | Description |
|---|---|---|
direction | "top" | "right" | "bottom" | "left" | The side from which the drawer will appear. Default is "bottom". This prop is an alias for Base UI's swipeDirection. If both are specified, swipeDirection wins. |
See the Base UI - Drawer.Root for more props.
DrawerTrigger
A button that opens the drawer. Renders a <button> element.
See the Base UI - Drawer.Trigger.
DrawerContent
A container for the drawer contents. Renders a <div> element.
See the Base UI - Drawer.Popup.
DrawerHeader
A container for the header of the drawer. Renders a <div> element.
DrawerTitle
A heading that labels the drawer. Renders an <h2> element.
See the Base UI - Drawer.Title.
DrawerDescription
A paragraph with additional information about the drawer. Renders a <p> element.
See the Base UI - Drawer.Description.
DrawerFooter
A container for the footer of the drawer. Renders a <div> element.
DrawerClose
A button that closes the drawer. Renders a <button> element.
See the Base UI - Drawer.Close.