Components

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.
Top bar
Context view

Installation

Install the following dependencies:

npm install vaul

Create 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

Compose Drawer, 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

Set direction 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).
Top bar
Context view

API Reference

Drawer (root)

activeSnapPointstring | number | null
Default: -
setActiveSnapPoint((snapPoint: string | number | null) => void)
Default: -
openboolean
Default: -
closeThresholdnumber
Default: 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.

noBodyStylesboolean
Default: -

When `true` the `body` doesn't get any styles assigned from Vaul

onOpenChange((open: boolean) => void)
Default: -
shouldScaleBackgroundboolean
Default: -
setBackgroundColorOnScaleboolean
Default: true

When `false` we don't change body's background color when the drawer is open.

scrollLockTimeoutnumber
Default: 500ms

Duration for which the drawer is not draggable after scrolling content inside of the drawer.

fixedboolean
Default: -

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

handleOnlyboolean
Default: false

When `true` only allows the drawer to be dragged by the `<Drawer.Handle />` component.

dismissibleboolean
Default: 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)
Default: -
onRelease((event: PointerEvent<HTMLDivElement>, open: boolean) => void)
Default: -
modalboolean
Default: true

When `false` it allows to interact with elements outside of the drawer without closing it.

nestedboolean
Default: -
onClose(() => void)
Default: -
direction"left" | "right" | "top" | "bottom"
Default: 'bottom'

Direction of the drawer. Can be `top` or `bottom`, `left`, `right`.

defaultOpenboolean
Default: false

Opened by default, skips initial enter animation. Still reacts to `open` state changes

disablePreventScrollboolean
Default: false

When set to `true` prevents scrolling on the document body on mount, and restores it on unmount.

repositionInputsboolean
Default: 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.

snapToSequentialPointboolean
Default: 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.

containerHTMLElement | null
Default: -
onAnimationEnd((open: boolean) => void)
Default: -

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.

preventScrollRestorationboolean
Default: -
autoFocusboolean
Default: -
PropTypeDefaultDescription
activeSnapPointstring | number | null--
setActiveSnapPoint((snapPoint: string | number | null) => void)--
openboolean--
closeThresholdnumber0.25Number 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.
noBodyStylesboolean-When `true` the `body` doesn't get any styles assigned from Vaul
onOpenChange((open: boolean) => void)--
shouldScaleBackgroundboolean--
setBackgroundColorOnScalebooleantrueWhen `false` we don't change body's background color when the drawer is open.
scrollLockTimeoutnumber500msDuration for which the drawer is not draggable after scrolling content inside of the drawer.
fixedboolean-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
handleOnlybooleanfalseWhen `true` only allows the drawer to be dragged by the `<Drawer.Handle />` component.
dismissiblebooleantrueWhen `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)--
modalbooleantrueWhen `false` it allows to interact with elements outside of the drawer without closing it.
nestedboolean--
onClose(() => void)--
direction"left" | "right" | "top" | "bottom"'bottom'Direction of the drawer. Can be `top` or `bottom`, `left`, `right`.
defaultOpenbooleanfalseOpened by default, skips initial enter animation. Still reacts to `open` state changes
disablePreventScrollbooleanfalseWhen set to `true` prevents scrolling on the document body on mount, and restores it on unmount.
repositionInputsbooleantrue when {@link snapPoints } is definedWhen `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.
snapToSequentialPointbooleanfalseDisabled 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.
containerHTMLElement | 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.
preventScrollRestorationboolean--
autoFocusboolean--
PropTypeDescription
openbooleanControlled open state.
onOpenChange(open: boolean) => voidCalled when open state changes.
direction"top" | "bottom" | "left" | "right"Slide direction. Default: "bottom".

DrawerTrigger, DrawerContent, DrawerClose

Use DrawerTrigger 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 in DrawerHeader, actions in DrawerFooter. DrawerTitle and DrawerDescription are used for screen readers.

TopBar (app branding)

Separate component used as the drawer header. TopBar accepts a variant: "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 accepts variant: "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.

DrawerBar, DrawerTitleBlock, DrawerBody

DrawerBar is a simple header bar (no variants). DrawerTitleBlock wraps the title, secondary text, and optional link. DrawerBody is the main scrollable area below a divider. Prefer TopBar for app-branding variants.

On this page