Drawer
A panel that slides in from the edge of the screen with swipe-to-dismiss gestures.
'use client';
import { Button } from '@/components/ui/button';
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from '@/components/ui/drawer';
export function DrawerDefault() {
return (
<Drawer>
<DrawerTrigger render={<Button variant='outline' />}>
Open Drawer
</DrawerTrigger>
<DrawerPopup>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Notifications</DrawerTitle>
<DrawerDescription>
You are all caught up. Good job!
</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<DrawerClose render={<Button variant='outline' />}>
Close
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</DrawerPopup>
</Drawer>
);
}
Installation
Usage
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer"<Drawer>
<DrawerTrigger>Open</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Drawer Title</DrawerTitle>
<DrawerDescription>Drawer description goes here.</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<DrawerClose>Close</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>Examples
Right Side
Use swipeDirection="right" on the Drawer component to slide the panel in from the right.
'use client';
import { Button } from '@/components/ui/button';
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from '@/components/ui/drawer';
export function DrawerRight() {
return (
<Drawer swipeDirection='right'>
<DrawerTrigger render={<Button variant='outline' />}>
Open Drawer
</DrawerTrigger>
<DrawerPopup>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Drawer</DrawerTitle>
<DrawerDescription>
This is a drawer that slides in from the side. You can swipe to
dismiss it.
</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<DrawerClose render={<Button variant='outline' />}>
Close
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</DrawerPopup>
</Drawer>
);
}
Non-Modal
Set modal={false} and disablePointerDismissal on the Drawer to keep the page interactive while the drawer is open.
'use client';
import { Button } from '@/components/ui/button';
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from '@/components/ui/drawer';
export function DrawerNonModal() {
return (
<Drawer swipeDirection='right' modal={false} disablePointerDismissal>
<DrawerTrigger render={<Button variant='outline' />}>
Open Non-Modal Drawer
</DrawerTrigger>
<DrawerPopup>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Non-modal Drawer</DrawerTitle>
<DrawerDescription>
This drawer does not trap focus and ignores outside clicks. Use
the close button or swipe to dismiss it.
</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<DrawerClose render={<Button variant='outline' />}>
Close
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</DrawerPopup>
</Drawer>
);
}
Nested Drawers
Nest multiple Drawer components and manage each with controlled open / onOpenChange state to create a stacking effect.
'use client';
import * as React from 'react';
import { Button } from '@/components/ui/button';
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from '@/components/ui/drawer';
export function DrawerNested() {
const [firstOpen, setFirstOpen] = React.useState(false);
const [secondOpen, setSecondOpen] = React.useState(false);
const [thirdOpen, setThirdOpen] = React.useState(false);
return (
<Drawer
open={firstOpen}
onOpenChange={(nextOpen) => {
setFirstOpen(nextOpen);
if (!nextOpen) {
setSecondOpen(false);
setThirdOpen(false);
}
}}
>
<DrawerTrigger render={<Button variant='outline' />}>
Open Drawer Stack
</DrawerTrigger>
<DrawerPopup>
<DrawerContent>
<DrawerTitle className='mb-1'>Account</DrawerTitle>
<DrawerDescription className='mb-6'>
Nested drawers can be styled to stack, while each drawer remains
independently focus managed.
</DrawerDescription>
<div className='flex items-center justify-end gap-4'>
<div className='mr-auto'>
<Drawer
open={secondOpen}
onOpenChange={(nextOpen) => {
setSecondOpen(nextOpen);
if (!nextOpen) {
setThirdOpen(false);
}
}}
>
<DrawerTrigger
render={<Button variant='link' className='px-0' />}
>
Security settings
</DrawerTrigger>
<DrawerPopup>
<DrawerContent>
<DrawerTitle className='mb-1'>Security</DrawerTitle>
<DrawerDescription className='mb-6'>
Review sign-in activity and update your security
preferences.
</DrawerDescription>
<ul className='mb-6 list-disc pl-5 text-muted-foreground'>
<li>Passkeys enabled</li>
<li>2FA via authenticator app</li>
<li>3 signed-in devices</li>
</ul>
<div className='flex items-center justify-end gap-4'>
<div className='mr-auto'>
<Drawer open={thirdOpen} onOpenChange={setThirdOpen}>
<DrawerTrigger
render={<Button variant='link' className='px-0' />}
>
Advanced options
</DrawerTrigger>
<DrawerPopup>
<DrawerContent>
<DrawerTitle className='mb-1'>
Advanced
</DrawerTitle>
<DrawerDescription className='mb-6'>
This drawer is taller to demonstrate
variable-height stacking.
</DrawerDescription>
<div className='mb-4 grid gap-1.5'>
<label
className='text-sm font-medium'
htmlFor='device-name'
>
Device name
</label>
<input
id='device-name'
className='w-full rounded-md border border-input bg-background px-2.5 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none'
defaultValue='Personal laptop'
/>
</div>
<div className='mb-6 grid gap-1.5'>
<label
className='text-sm font-medium'
htmlFor='notes'
>
Notes
</label>
<textarea
id='notes'
className='w-full resize-y rounded-md border border-input bg-background px-2.5 py-2 text-sm focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none'
defaultValue='Rotate recovery codes and revoke older sessions.'
rows={3}
/>
</div>
<div className='flex justify-end'>
<DrawerClose
render={<Button variant='outline' />}
>
Done
</DrawerClose>
</div>
</DrawerContent>
</DrawerPopup>
</Drawer>
</div>
<DrawerClose render={<Button variant='outline' />}>
Close
</DrawerClose>
</div>
</DrawerContent>
</DrawerPopup>
</Drawer>
</div>
<DrawerClose render={<Button variant='outline' />}>
Close
</DrawerClose>
</div>
</DrawerContent>
</DrawerPopup>
</Drawer>
);
}
Snap Points
Pass a snapPoints array to Drawer and use unstyled on DrawerPopup to apply custom popup styling for snap behavior.
'use client';
import * as React from 'react';
import { Button } from '@/components/ui/button';
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from '@/components/ui/drawer';
const TOP_MARGIN_REM = 1;
const VISIBLE_SNAP_POINTS_REM = [30];
function toViewportSnapPoint(heightRem: number) {
return `${heightRem + TOP_MARGIN_REM}rem`;
}
const snapPoints = [...VISIBLE_SNAP_POINTS_REM.map(toViewportSnapPoint), 1];
export function DrawerSnapPoints() {
return (
<Drawer snapPoints={snapPoints}>
<DrawerTrigger render={<Button variant='outline' />}>
Open Snap Drawer
</DrawerTrigger>
<DrawerPopup
unstyled
className='relative flex max-h-[calc(100dvh-var(--top-margin))] min-h-0 w-full transform-[translateY(calc(var(--drawer-snap-point-offset)+var(--drawer-swipe-movement-y)))] touch-none flex-col overflow-visible rounded-t-xl bg-background pb-[max(0px,calc(var(--drawer-snap-point-offset)+var(--drawer-swipe-movement-y)))] text-foreground shadow-[0_-16px_48px_rgb(0_0_0/0.12),0_6px_18px_rgb(0_0_0/0.06)] ring-1 ring-foreground/10 transition-[transform,box-shadow] duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] outline-none [--bleed:3rem] after:pointer-events-none after:absolute after:inset-x-0 after:top-full after:h-(--bleed) after:bg-background after:content-[""] data-ending-style:transform-[translateY(100%)] data-ending-style:pb-0 data-ending-style:shadow-[0_-16px_48px_rgb(0_0_0/0),0_6px_18px_rgb(0_0_0/0)] data-ending-style:duration-[calc(var(--drawer-swipe-strength)*300ms)] data-starting-style:transform-[translateY(100%)] data-starting-style:pb-0 data-starting-style:shadow-[0_-16px_48px_rgb(0_0_0/0),0_6px_18px_rgb(0_0_0/0)] data-swiping:transition-none data-swiping:select-none'
style={
{ '--top-margin': `${TOP_MARGIN_REM}rem` } as React.CSSProperties
}
>
<div className='shrink-0 touch-none border-b border-border px-6 pt-3.5 pb-3'>
<div className='mx-auto h-1 w-12 rounded-full bg-muted-foreground/30' />
<DrawerTitle className='mt-2.5 cursor-default'>
Snap points
</DrawerTitle>
</div>
<DrawerContent className='min-h-0 flex-1 touch-auto overflow-y-auto overscroll-contain px-6 pt-4 pb-[calc(1.5rem+env(safe-area-inset-bottom,0px))]'>
<div className='mx-auto w-full max-w-87.5'>
<DrawerDescription className='mb-4'>
Drag the sheet to snap between a compact peek and a near
full-height view.
</DrawerDescription>
<div className='mb-6 grid gap-3' aria-hidden>
{Array.from({ length: 20 }, (_, index) => (
<div
key={index}
className='h-12 rounded-xl border border-border bg-muted'
/>
))}
</div>
<div className='flex items-center justify-end gap-4'>
<DrawerClose render={<Button variant='outline' />}>
Close
</DrawerClose>
</div>
</div>
</DrawerContent>
</DrawerPopup>
</Drawer>
);
}
Action Sheet
Use DrawerPopup with unstyled and overlayClassName to customize the popup and overlay styling for an action sheet layout.
'use client';
import * as React from 'react';
import { Button } from '@/components/ui/button';
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerPopup,
DrawerTitle,
DrawerTrigger,
} from '@/components/ui/drawer';
const ACTIONS = [
'Unfollow',
'Mute',
'Add to Favourites',
'Add to Close Friends',
'Restrict',
];
export function DrawerActionSheet() {
const [open, setOpen] = React.useState(false);
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger render={<Button variant='outline' />}>
Open Action Sheet
</DrawerTrigger>
<DrawerPopup
unstyled
overlayClassName='bg-black/40'
className='pointer-events-none relative flex w-full max-w-md transform-[translateY(var(--drawer-swipe-movement-y))] touch-auto flex-col gap-3 overflow-y-auto overscroll-contain bg-transparent px-4 pt-4 pb-[calc(1rem+env(safe-area-inset-bottom,0px))] text-foreground ring-0 transition-transform duration-300 ease-[cubic-bezier(0.32,0.72,0,1)] outline-none data-ending-style:transform-[translateY(calc(100%+1rem))] data-ending-style:duration-[calc(var(--drawer-swipe-strength)*300ms)] data-starting-style:transform-[translateY(calc(100%+1rem))] data-swiping:transition-none data-swiping:select-none'
>
<DrawerContent className='pointer-events-auto overflow-hidden rounded-2xl bg-background ring-1 ring-foreground/10'>
<DrawerTitle className='sr-only'>Profile actions</DrawerTitle>
<DrawerDescription className='sr-only'>
Choose an action for this user.
</DrawerDescription>
<ul
className='m-0 list-none divide-y divide-foreground/10 p-0'
aria-label='Profile actions'
>
{ACTIONS.map((action, index) => (
<li key={action}>
{index === 0 && (
<DrawerClose className='sr-only'>
Close action sheet
</DrawerClose>
)}
<button
type='button'
className='block w-full border-0 bg-transparent px-5 py-4 text-center text-sm select-none hover:bg-muted focus-visible:bg-muted focus-visible:outline-none'
onClick={() => setOpen(false)}
>
{action}
</button>
</li>
))}
</ul>
</DrawerContent>
<div className='pointer-events-auto overflow-hidden rounded-2xl bg-background ring-1 ring-foreground/10'>
<button
type='button'
className='block w-full border-0 bg-transparent px-5 py-4 text-center text-sm text-destructive select-none hover:bg-muted focus-visible:bg-muted focus-visible:outline-none'
onClick={() => setOpen(false)}
>
Block User
</button>
</div>
</DrawerPopup>
</Drawer>
);
}