Components
- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Collapsible
- Combobox
- Command
- Context Menu
- Date Picker
- Dialog
- Drawer
- Dropdown Menu
- Empty
- Field
- Hover Card
- Input
- Input OTP
- Input Group
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Native Select
- Navigation Menu
- Pagination
- Popover
- Progress
- Radio Group
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner
- Spinner
- Switch
- Table
- Tabs
- Textarea
- Toggle
- Toggle Group
- Tooltip
- New Component
Elements
Charts
Icons
import { Button } from "@/components/ui/button";
import { Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from "@/components/ui/drawer";
export const DrawerDemo = () => (
<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">
<div className="flex items-center justify-center space-x-2">
<div className="flex-1 text-center">
<div className="text-7xl font-bold tracking-tighter">350</div>
<div className="text-[0.70rem] uppercase text-muted-foreground">Calories/day</div>
</div>
</div>
</div>
<DrawerFooter>
<Button>Submit</Button>
<DrawerClose asChild><Button variant="outline">Cancel</Button></DrawerClose>
</DrawerFooter>
</div>
</DrawerContent>
</Drawer>
);Direction
The direction prop controls which edge the drawer slides in from. Default is "bottom".
{/* Bottom (default) — drag handle shown automatically */}
<Drawer direction="bottom">...</Drawer>
{/* Right — sidebar style */}
<Drawer direction="right">...</Drawer>
{/* Left */}
<Drawer direction="left">...</Drawer>
{/* Top */}
<Drawer direction="top">...</Drawer>The drag handle (the pill at the top) only appears on direction="bottom" drawers via the group-data-[vaul-drawer-direction=bottom] selector.
Right drawer
<Drawer direction="right">
<DrawerTrigger asChild>
<Button variant="outline">Open right drawer</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Settings</DrawerTitle>
<DrawerDescription>Adjust your preferences.</DrawerDescription>
</DrawerHeader>
<div className="flex-1 overflow-y-auto p-4">
{/* content */}
</div>
<DrawerFooter>
<DrawerClose asChild>
<Button variant="outline">Close</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>Responsive: Drawer on mobile, Dialog on desktop
A common pattern — use useMediaQuery (or a CSS approach) to swap between Drawer and Dialog depending on screen width.
"use client";
import { useMediaQuery } from "@/hooks/use-media-query";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { useState } from "react";
export function ResponsiveDrawer() {
const [open, setOpen] = useState(false);
const isDesktop = useMediaQuery("(min-width: 768px)");
if (isDesktop) {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button variant="outline">Edit profile</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-106.25">
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here.
</DialogDescription>
</DialogHeader>
{/* form */}
</DialogContent>
</Dialog>
);
}
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
<Button variant="outline">Edit profile</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader className="text-left">
<DrawerTitle>Edit profile</DrawerTitle>
<DrawerDescription>
Make changes to your profile here.
</DrawerDescription>
</DrawerHeader>
{/* form */}
<DrawerFooter className="pt-2">
<DrawerClose asChild>
<Button variant="outline">Cancel</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}With sounds
Pass sounds to enable open/close audio feedback.
<Drawer sounds>
<DrawerTrigger asChild>
<Button>Open</Button>
</DrawerTrigger>
<DrawerContent>...</DrawerContent>
</Drawer>Props
Drawer
| Prop | Type | Default | Description |
|---|---|---|---|
direction | "top" | "bottom" | "left" | "right" | "bottom" | Which edge to slide in from |
open | boolean | — | Controlled open state |
defaultOpen | boolean | false | Uncontrolled initial state |
onOpenChange | (open: boolean) => void | — | Callback when open state changes |
sounds | boolean | false | Play open/close audio feedback |
shouldScaleBackground | boolean | false | Scale the page content behind the drawer |
dismissible | boolean | true | Allow closing by dragging down or clicking overlay |
Usage
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
<Drawer>
<DrawerTrigger asChild>
<Button>Open drawer</Button>
</DrawerTrigger>
<DrawerContent>
<div className="mx-auto w-full max-w-sm">
<DrawerHeader>
<DrawerTitle>Title</DrawerTitle>
<DrawerDescription>Description.</DrawerDescription>
</DrawerHeader>
<div className="p-4">{/* content */}</div>
<DrawerFooter>
<Button>Confirm</Button>
<DrawerClose asChild>
<Button variant="outline">Cancel</Button>
</DrawerClose>
</DrawerFooter>
</div>
</DrawerContent>
</Drawer>Specs
# Mint — Drawer Specs
## Component
- name: drawer
- file: registry/mint/drawer.tsx
- installs to: components/ui/drawer.tsx
- dependencies: vaul
- registryDependencies: none
## What is it
A panel that slides in from any edge of the screen, built on Vaul. On touch
devices it is draggable and dismissable by swipe or overlay click; on desktop
it behaves as a fixed side or bottom panel. Prefer Drawer over Dialog on
mobile — consider swapping to Dialog on desktop via useMediaQuery.
## Visual identity
Flat --background surface with no elevation shadow; border on the open edge
provides separation. The bottom variant shows a muted pill drag handle.
Rounded corners only on the open edge match Mint's 1.25rem radius feel.
## Design tokens
--background: oklch(1 0 0) / oklch(0.145 0 0) — panel fill
--foreground: oklch(0.145 0 0) / oklch(0.985 0 0) — DrawerTitle text
--muted: oklch(0.96 0.02 155) / oklch(0.2 0.03 155) — drag handle pill fill
--muted-foreground: oklch(0.52 0.06 155) / oklch(0.6 0.06 155) — DrawerDescription text
--border: oklch(0.88 0.04 155 / 60%) / oklch(0.32 0.05 155 / 40%) — edge border on DrawerContent
## Sub-components
- Drawer → Vaul root; accepts direction, sounds, open, onOpenChange, dismissible, shouldScaleBackground
- DrawerTrigger → Vaul Trigger; use asChild to wrap any element
- DrawerPortal → renders DrawerOverlay + DrawerContent outside the DOM tree
- DrawerOverlay → fixed inset-0 z-50 bg-black/50; fades in/out with open state
- DrawerContent → fixed panel; direction-aware sizing and border via data-[vaul-drawer-direction=*]; drag handle pill auto-shown for bottom only
- DrawerHeader → flex-col gap-0.5 p-4; text centred for bottom/top, left-aligned at md+ and for side drawers
- DrawerTitle → font-semibold text-foreground; required for accessibility
- DrawerDescription → text-sm text-muted-foreground; strongly recommended for accessibility
- DrawerFooter → mt-auto flex-col gap-2 p-4; stacks actions to the bottom of the panel
- DrawerClose → Vaul Close; use asChild wrapping a Button for cancel/close
## States
- data-[state=open]: DrawerOverlay animates in with fade-in-0
- data-[state=closed]: DrawerOverlay animates out with fade-out-0
- data-[vaul-drawer-direction=bottom]: DrawerContent inset-x-0, bottom-0, mt-24, max-h-[80vh], rounded-t-lg, border-t
- data-[vaul-drawer-direction=top]: DrawerContent inset-x-0, top-0, mb-24, max-h-[80vh], rounded-b-lg, border-b
- data-[vaul-drawer-direction=right]: DrawerContent inset-y-0, right-0, w-3/4, sm:max-w-sm, border-l
- data-[vaul-drawer-direction=left]: DrawerContent inset-y-0, left-0, w-3/4, sm:max-w-sm, border-r
## Conventions
- sounds: GIN Design exclusive — plays drawerOpen/drawerClose audio via useFeedback hook on open/close
- Sub-components expose data-slot="drawer", "drawer-trigger", "drawer-portal", "drawer-overlay", "drawer-content", "drawer-header", "drawer-title", "drawer-description", "drawer-footer", "drawer-close" for CSS targeting
- Wrap DrawerContent body in <div className="mx-auto w-full max-w-sm"> for bottom drawers to constrain width
- Always include DrawerClose (wrapping a Button variant="outline") in DrawerFooter as the cancel action
- For right/left drawers with scrollable content, add overflow-y-auto to the inner content div
- DrawerTitle is required; DrawerDescription is strongly recommended — both are needed for screen-reader announcements
- Responsive pattern: render Drawer on mobile (direction="bottom") and Dialog on desktop — use useMediaQuery to swap, not CSS hidingInstallation
Drawer is built on Vaul — a native-feeling drawer library for React. It is included as a dependency.
$ npx shadcn add \https://gin-design.vercel.app/drawer