Drawer
A slide-in panel that overlays the main content.
'use client';
import { Button } from '@/components/button';
import {
Drawer,
DrawerActions,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from '@/components/drawer';
const DrawerPreview = () => {
return (
<Drawer>
<DrawerTrigger asChild>
<Button>Open Drawer</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Drawer Title</DrawerTitle>
</DrawerHeader>
<DrawerDescription>Drawer content goes here.</DrawerDescription>
<DrawerActions className="flex gap-2">
<DrawerClose asChild>
<Button variant="primary" className="grow">
Submit
</Button>
</DrawerClose>
<DrawerClose asChild>
<Button className="grow" variant="outline">
Close
</Button>
</DrawerClose>
</DrawerActions>
</DrawerContent>
</Drawer>
);
};
export default DrawerPreview; Dependencies
Source Code
import {
Modal,
ModalClose,
ModalContent,
ModalDescription,
ModalTitle,
ModalTrigger,
} from '@/components/modal';
import { cn } from '@/lib/utils/classnames';
type DrawerProps = React.ComponentProps<typeof Modal>;
const Drawer = (props: DrawerProps) => {
return <Modal {...props} />;
};
type DrawerContentProps = React.ComponentProps<typeof ModalContent>;
const DrawerContent = ({
className,
children,
...props
}: DrawerContentProps) => {
return (
<ModalContent
className={cn(
'[--drawer-p:--spacing(4)]',
'overflow-x-hidden! mx-auto flex w-full max-w-screen flex-col overflow-y-auto border border-border bg-background-high p-(--drawer-p) *:shrink-0',
'has-[[data-modal-focus-catcher]:first-child+[data-drawer-header],[data-drawer-header]:first-child]:pt-0 has-[[data-drawer-actions]:last-child]:pb-0',
'backdrop:bg-black/20 not-data-[status=open]:backdrop:opacity-0 backdrop:backdrop-blur-sm',
// desktop
'md:mr-0 md:h-full md:max-h-screen md:w-full md:max-w-lg md:not-data-[status=open]:translate-x-full',
// mobile
'min-h-[50svh] max-md:mb-0 max-md:max-h-[calc(100svh-(--spacing(16)))] max-md:w-full max-md:not-data-[status=open]:translate-y-full max-md:rounded-t-xl',
// animation props
'transition-transform ease-emphasized-decelerate not-data-[status=open]:ease-emphasized-accelerate',
'backdrop:transition-opacity backdrop:ease-in-out',
'motion-reduce:transition-none motion-reduce:backdrop:transition-none',
'duration-400 not-data-[status=open]:duration-250 not-data-[status=open]:backdrop:delay-150 not-data-[status=open]:backdrop:duration-200',
className
)}
{...props}
>
{children}
</ModalContent>
);
};
const DrawerTrigger = ModalTrigger;
const DrawerClose = ModalClose;
const DrawerTitle = ({
children,
className,
...props
}: React.ComponentProps<typeof ModalTitle>) => {
return (
<ModalTitle className={cn('font-semibold', className)} {...props}>
{children}
</ModalTitle>
);
};
const DrawerDescription = ({
children,
className,
...props
}: React.ComponentProps<typeof ModalDescription>) => {
return (
<ModalDescription className={cn('pb-2', className)} {...props}>
{children}
</ModalDescription>
);
};
const DrawerBleed = ({
className,
...props
}: React.ComponentPropsWithRef<'div'>) => {
return <div className={cn('-mx-(--drawer-p)', className)} {...props} />;
};
const DrawerHeader = ({
className,
children,
...props
}: React.ComponentPropsWithRef<'div'>) => {
return (
<DrawerBleed
className={cn(
'sticky top-0 z-10 mb-(--drawer-p) border-border border-b bg-background p-(--drawer-p)',
className
)}
{...props}
data-drawer-header=""
>
{children}
</DrawerBleed>
);
};
const DrawerActions = ({
className,
children,
...props
}: React.ComponentPropsWithRef<'div'>) => {
return (
<DrawerBleed
className={cn(
'sticky bottom-0 mt-auto flex gap-2 border-border border-t bg-background p-(--drawer-p)',
className
)}
{...props}
data-drawer-actions=""
>
{children}
</DrawerBleed>
);
};
export {
Drawer,
DrawerActions,
DrawerBleed,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
}; Features
- Modal Overlay: Creates an accessible modal dialog with backdrop
- Focus Management: Automatically traps focus within the dialog
- Flexible Positioning: Center or top alignment options
- Controlled & Uncontrolled: Supports both controlled and uncontrolled modes
- Customizable Actions: Built-in support for common dialog actions
Anatomy
<Drawer>
<DrawerTrigger />
<DrawerContent>
<DrawerHeader>
<DrawerTitle />
</DrawerHeader>
<DrawerDescription />
<DrawerActions>
<DrawerClose />
</DrawerActions>
</DrawerContent>
</Drawer>
API Reference
Drawer
Extends the Modal component.
DrawerContent
Extends the ModalContent component.
DrawerTrigger
Extends the ModalTrigger component.
DrawerClose
Extends the ModalClose component.
DrawerTitle
Extends the ModalTitle component.
DrawerDescription
Extends the ModalDescription component.
DrawerHeader
Extends the div element.
DrawerActions
Extends the div element.
Examples
Simple
'use client';
import { Button } from '@/components/button';
import {
Drawer,
DrawerActions,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from '@/components/drawer';
const DrawerPreview = () => {
return (
<Drawer>
<DrawerTrigger asChild>
<Button>Open Drawer</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Drawer Title</DrawerTitle>
</DrawerHeader>
<DrawerDescription>Drawer content goes here.</DrawerDescription>
<DrawerActions className="flex gap-2">
<DrawerClose asChild>
<Button variant="primary" className="grow">
Submit
</Button>
</DrawerClose>
<DrawerClose asChild>
<Button className="grow" variant="outline">
Close
</Button>
</DrawerClose>
</DrawerActions>
</DrawerContent>
</Drawer>
);
};
export default DrawerPreview; Tall Content
'use client';
import { Button } from '@/components/button';
import {
Drawer,
DrawerActions,
DrawerClose,
DrawerContent,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from '@/components/drawer';
const DrawerTallContent = () => {
return (
<Drawer>
<DrawerTrigger asChild>
<Button>Open Drawer</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Drawer Title</DrawerTitle>
</DrawerHeader>
{Array(24)
.fill(null)
.map((_, index) => (
<p key={index} className="mb-4">
This is paragraph {index + 1}.
</p>
))}
<DrawerActions className="flex gap-2">
<DrawerClose asChild>
<Button className="grow">Submit</Button>
</DrawerClose>
<DrawerClose asChild>
<Button className="grow" variant="outline">
Close
</Button>
</DrawerClose>
</DrawerActions>
</DrawerContent>
</Drawer>
);
};
export default DrawerTallContent; Controlled
'use client';
import { useState } from 'react';
import { Button } from '@/components/button';
import {
Drawer,
DrawerActions,
DrawerContent,
DrawerHeader,
DrawerTitle,
} from '@/components/drawer';
const DrawerControlled = () => {
const [open, setOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = () => {
setIsSubmitting(true);
setTimeout(() => {
setIsSubmitting(false);
setOpen(false);
}, 3000);
};
const handleCancel = () => {
setOpen(false);
};
return (
<Drawer
open={open}
onOpenChange={(isOpen) => setOpen(isSubmitting ? true : isOpen)}
>
<Button onClick={() => setOpen(true)}>Open Drawer</Button>
<DrawerContent inert={isSubmitting}>
<DrawerHeader>
<DrawerTitle>Drawer Title</DrawerTitle>
</DrawerHeader>
<p>Drawer dangerous content goes here.</p>
<DrawerActions className="flex gap-2">
<Button
className="grow"
variant="destructive"
isLoading={isSubmitting}
onClick={handleSubmit}
disabled={isSubmitting}
>
Delete Everything
</Button>
<Button
className="grow"
variant="outline"
onClick={handleCancel}
disabled={isSubmitting}
>
I'm not sure
</Button>
</DrawerActions>
</DrawerContent>
</Drawer>
);
};
export default DrawerControlled; Best Practices
-
Content Structure:
- Always include a clear title that describes the purpose
- Keep content concise and focused
- Use appropriate action labels (avoid “OK/Cancel”)
-
Performance:
- Lazy load dialog content if needed
- Consider using dynamic imports for heavy content
- Clean up resources when dialog closes
Previous
Divider
Next
Dropdown