Skip to content

Drawer

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.

Right Drawer (Default)

View 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'.

Left Drawer

View 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>

Drawer with Footer

View 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:

ComponentDescription
DrawerRoot component that manages open/close state (Radix Dialog.Root)
DrawerTriggerElement that opens the drawer when clicked (Radix Dialog.Trigger)
DrawerPortalPortals content to document body (Radix Dialog.Portal)
DrawerOverlaySemi-transparent backdrop behind the drawer
DrawerContentThe drawer panel itself (includes Portal and Overlay automatically)
DrawerHeaderHeader area for title and description
DrawerBodyMain content area
DrawerTitleAccessible title text (Radix Dialog.Title)
DrawerDescriptionAccessible description text (Radix Dialog.Description)
DrawerCloseElement 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, and aria-describedby
  • Slide animations: Open and close animations using CSS data-state selectors

Keyboard Support

KeyAction
EscapeCloses the drawer
TabMoves focus to next focusable element within drawer
Shift + TabMoves focus to previous focusable element within drawer

Best Practices

  • Always include a DrawerTitle for screen reader accessibility
  • Provide a visible close mechanism (close button, cancel action, or overlay click)
  • Use side="left" for navigation menus and side="right" for detail panels and forms
  • Consider using DrawerDescription for additional context
  • Modal - Centered dialog overlay for focused interactions
  • Card - Non-overlay content container
  • Tabs - Organize content without overlays