Drawer
A panel that slides up from the bottom of the screen. Built on Vaul.
Installation
bash
npx shadcn@latest add drawerImport
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
openboolean—Controlled open stateonOpenChange(open: boolean) => void—Called when open state changesshouldScaleBackgroundbooleanfalseScales the background page when drawer opensdismissiblebooleantrueWhether swipe-down closes the drawerNotes
- 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