Drawer
Slide-in side panel overlay for navigation, forms, and supplementary content
The Drawer component provides a slide-in panel from the left or right side of the screen. Built on Radix UI Dialog, it shares the same accessibility foundation as the Modal component -- including focus trapping, keyboard navigation, and screen reader support -- but uses a side-panel layout instead of a centered dialog.
Import
import {
Drawer,
DrawerTrigger,
DrawerContent,
DrawerOverlay,
DrawerHeader,
DrawerBody,
DrawerTitle,
DrawerDescription,
DrawerClose,
DrawerPortal,
} from '@nim-ui/components';Basic Usage
The simplest drawer uses Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle, and DrawerBody.
Code
<Drawer>
<DrawerTrigger asChild>
<Button>Open Right Drawer</Button>
</DrawerTrigger>
<DrawerContent side="right">
<DrawerHeader>
<DrawerTitle>Right Drawer</DrawerTitle>
<DrawerDescription>This drawer slides in from the right side.</DrawerDescription>
</DrawerHeader>
<DrawerBody>
<p>Right drawer content here.</p>
</DrawerBody>
</DrawerContent>
</Drawer>Side Variants
The side prop on DrawerContent controls which edge the drawer slides in from. It defaults to 'right'.
Code
<Drawer>
<DrawerTrigger asChild>
<Button variant="outline">Open Left Drawer</Button>
</DrawerTrigger>
<DrawerContent side="left">
<DrawerHeader>
<DrawerTitle>Navigation</DrawerTitle>
<DrawerDescription>Browse the menu items below.</DrawerDescription>
</DrawerHeader>
<DrawerBody>
<nav>...</nav>
</DrawerBody>
</DrawerContent>
</Drawer>With Footer Actions
Code
<Drawer>
<DrawerTrigger asChild>
<Button variant="secondary">Open with Footer</Button>
</DrawerTrigger>
<DrawerContent side="right">
<DrawerHeader>
<DrawerTitle>Edit Profile</DrawerTitle>
<DrawerDescription>Make changes to your profile here.</DrawerDescription>
</DrawerHeader>
<DrawerBody>
<p>Profile editing form would go here.</p>
<div className="flex gap-3 justify-end mt-4">
<DrawerClose asChild>
<Button variant="outline">Cancel</Button>
</DrawerClose>
<Button>Save Changes</Button>
</div>
</DrawerBody>
</DrawerContent>
</Drawer>Controlled Drawer
Control the drawer open state externally using React state.
function ControlledDrawer() {
const [open, setOpen] = useState(false);
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
<Button>Open Controlled Drawer</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Controlled Drawer</DrawerTitle>
</DrawerHeader>
<DrawerBody>
<p>This drawer is controlled via React state.</p>
<Button onClick={() => setOpen(false)} className="mt-4">
Close Programmatically
</Button>
</DrawerBody>
</DrawerContent>
</Drawer>
);
}Component Architecture
The Drawer is composed of several sub-components, each with a specific role:
| Component | Description |
|---|---|
Drawer | Root component that manages open/close state (Radix Dialog.Root) |
DrawerTrigger | Element that opens the drawer when clicked (Radix Dialog.Trigger) |
DrawerPortal | Portals content to document body (Radix Dialog.Portal) |
DrawerOverlay | Semi-transparent backdrop behind the drawer |
DrawerContent | The drawer panel itself (includes Portal and Overlay automatically) |
DrawerHeader | Header area for title and description |
DrawerBody | Main content area |
DrawerTitle | Accessible title text (Radix Dialog.Title) |
DrawerDescription | Accessible description text (Radix Dialog.Description) |
DrawerClose | Element that closes the drawer when clicked (Radix Dialog.Close) |
Props
Drawer (Root)
| Name | Type | Default | Description |
|---|---|---|---|
open | boolean | - | Controlled open state of the drawer |
onOpenChange | (open: boolean) => void | - | Callback when the open state changes |
defaultOpen | boolean | false | Initial open state for uncontrolled usage |
modal | boolean | true | Whether to render as a modal (with inert background) |
children* | ReactNode | - | Drawer sub-components (Trigger, Content, etc.) |
DrawerContent
| Name | Type | Default | Description |
|---|---|---|---|
side | 'left' | 'right' | 'right' | Which side of the screen the drawer slides in from |
className | string | - | Additional CSS classes for custom styling |
onEscapeKeyDown | (event: KeyboardEvent) => void | - | Handler called when Escape key is pressed |
onPointerDownOutside | (event: PointerDownOutsideEvent) => void | - | Handler called when clicking outside the drawer |
children* | ReactNode | - | Drawer content (Header, Body, etc.) |
DrawerTitle
| Name | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes to apply to the title |
children* | ReactNode | - | Title text content |
DrawerDescription
| Name | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional CSS classes to apply to the description |
children* | ReactNode | - | Description text content |
Usage Examples
Mobile Navigation
function MobileNav() {
return (
<Drawer>
<DrawerTrigger asChild>
<button aria-label="Open menu" className="lg:hidden">
<MenuIcon />
</button>
</DrawerTrigger>
<DrawerContent side="left">
<DrawerHeader>
<DrawerTitle>Menu</DrawerTitle>
</DrawerHeader>
<DrawerBody>
<nav>
<Stack spacing="xs">
<a href="/" className="p-3 rounded hover:bg-neutral-100">Home</a>
<a href="/products" className="p-3 rounded hover:bg-neutral-100">Products</a>
<a href="/about" className="p-3 rounded hover:bg-neutral-100">About</a>
<a href="/contact" className="p-3 rounded hover:bg-neutral-100">Contact</a>
</Stack>
</nav>
</DrawerBody>
</DrawerContent>
</Drawer>
);
}Shopping Cart Drawer
function CartDrawer({ items, total }) {
return (
<Drawer>
<DrawerTrigger asChild>
<Button variant="ghost">
Cart ({items.length})
</Button>
</DrawerTrigger>
<DrawerContent side="right">
<DrawerHeader>
<DrawerTitle>Shopping Cart</DrawerTitle>
<DrawerDescription>{items.length} items</DrawerDescription>
</DrawerHeader>
<DrawerBody>
<Stack spacing="md">
{items.map((item) => (
<Flex key={item.id} justify="between" align="center">
<div>
<p className="font-medium">{item.name}</p>
<p className="text-sm text-neutral-500">Qty: {item.qty}</p>
</div>
<p className="font-semibold">${item.price}</p>
</Flex>
))}
<hr />
<Flex justify="between">
<span className="font-semibold">Total</span>
<span className="font-bold">${total}</span>
</Flex>
<Button variant="primary" fullWidth>
Checkout
</Button>
</Stack>
</DrawerBody>
</DrawerContent>
</Drawer>
);
}Detail Panel
function DetailPanel({ selectedItem }) {
const [open, setOpen] = useState(false);
useEffect(() => {
if (selectedItem) setOpen(true);
}, [selectedItem]);
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>{selectedItem?.title}</DrawerTitle>
</DrawerHeader>
<DrawerBody>
<Stack spacing="md">
<p>{selectedItem?.description}</p>
<Badge variant={selectedItem?.status === 'active' ? 'success' : 'default'}>
{selectedItem?.status}
</Badge>
</Stack>
</DrawerBody>
</DrawerContent>
</Drawer>
);
}Accessibility
The Drawer component inherits all accessibility features from Radix UI Dialog:
- Focus trapping: Focus is contained within the drawer while open
- Focus restoration: Focus returns to the trigger element when the drawer closes
- Escape key: Pressing Escape closes the drawer
- Background inert: Background content is made non-interactive while the drawer is open
- ARIA attributes: Automatically sets
role="dialog",aria-modal="true",aria-labelledby, andaria-describedby - Slide animations: Open and close animations using CSS data-state selectors
Keyboard Support
| Key | Action |
|---|---|
| Escape | Closes the drawer |
| Tab | Moves focus to next focusable element within drawer |
| Shift + Tab | Moves focus to previous focusable element within drawer |
Best Practices
- Always include a
DrawerTitlefor screen reader accessibility - Provide a visible close mechanism (close button, cancel action, or overlay click)
- Use
side="left"for navigation menus andside="right"for detail panels and forms - Consider using
DrawerDescriptionfor additional context