Drawer

A panel that slides up from the bottom of the screen. Built on Vaul.

Installation

bash
npx shadcn@latest add drawer

Import

import {  Drawer,  DrawerTrigger,  DrawerContent,  DrawerHeader,  DrawerFooter,  DrawerTitle,  DrawerDescription,  DrawerClose,} from "@/components/ui/drawer"

Basic (bottom sheet)

<Drawer>  <DrawerTrigger asChild>    <Button variant="outline">Open Drawer</Button>  </DrawerTrigger>  <DrawerContent>    <div className="mx-auto w-full max-w-sm">      <DrawerHeader>        <DrawerTitle>Move Goal</DrawerTitle>        <DrawerDescription>Set your daily activity goal.</DrawerDescription>      </DrawerHeader>      <div className="p-4 pb-0">        <p className="text-sm text-muted-foreground">Content goes here.</p>      </div>      <DrawerFooter>        <Button>Submit</Button>        <DrawerClose asChild>          <Button variant="outline">Cancel</Button>        </DrawerClose>      </DrawerFooter>    </div>  </DrawerContent></Drawer>

Responsive Dialog + Drawer

The recommended RDS pattern for any modal content — Dialog on desktop, Drawer on mobile.

import { useMediaQuery } from "@/hooks/use-media-query"import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from "@/components/ui/drawer"export function ResponsiveModal({ open, onOpenChange, children }) {  const isDesktop = useMediaQuery("(min-width: 768px)")  if (isDesktop) {    return (      <Dialog open={open} onOpenChange={onOpenChange}>        <DialogContent>          <DialogHeader><DialogTitle>Edit Profile</DialogTitle></DialogHeader>          {children}        </DialogContent>      </Dialog>    )  }  return (    <Drawer open={open} onOpenChange={onOpenChange}>      <DrawerContent>        <DrawerHeader><DrawerTitle>Edit Profile</DrawerTitle></DrawerHeader>        {children}      </DrawerContent>    </Drawer>  )}

useMediaQuery hook

Add this hook at hooks/use-media-query.ts for the responsive pattern above.

import { useEffect, useState } from "react"export function useMediaQuery(query: string) {  const [matches, setMatches] = useState(false)  useEffect(() => {    const media = window.matchMedia(query)    setMatches(media.matches)    const listener = () => setMatches(media.matches)    media.addEventListener("change", listener)    return () => media.removeEventListener("change", listener)  }, [query])  return matches}

Props

PROPTYPEDEFAULTDESCRIPTION
openbooleanControlled open state
onOpenChange(open: boolean) => voidCalled when open state changes
shouldScaleBackgroundbooleanfalseScales the background page when drawer opens
dismissiblebooleantrueWhether swipe-down closes the drawer

Notes

  • Drawer is mobile-first — default is a bottom sheet with drag handle
  • The Responsive Dialog+Drawer pattern above is the recommended RDS approach for any modal content