Drawer
A panel pinned to one edge of the viewport. A Dialog with different panel styling — everything about opening, closing, and labelling is identical.
Usage#
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "neelam-ui";
<Drawer>
<DrawerTrigger>Open filters</DrawerTrigger>
<DrawerContent side="right">
<DrawerHeader>
<DrawerTitle>Filters</DrawerTitle>
<DrawerDescription>Narrow the results.</DrawerDescription>
</DrawerHeader>
</DrawerContent>
</Drawer>Sides#
side picks the edge the panel slides in from — "right" by default.
left and right are full-height and capped at max-w-sm; top and bottom
are full-width and capped at 80vh, so a drawer never quite covers the page it
came from.
Built on Dialog#
DrawerContent is a real <dialog> opened with showModal(), so focus
trapping, Escape, inert background content, and top-layer stacking all come
from the browser rather than being re-implemented. DrawerTrigger,
DrawerClose, DrawerHeader, DrawerTitle, DrawerDescription, and
DrawerFooter are the same components as their Dialog
counterparts under drawer-flavoured names.
Why m-0 matters here
A <dialog> defaults to margin: auto. Left alone, that fights the panel's
own inset and h-dvh sizing: with top, height, and bottom all set, the
browser resolves the over-constrained box by splitting the leftover space into
equal margins — shrinking and centring the drawer instead of pinning it flush
to the edge.
Dismissal#
Unlike AlertDialog, a drawer is meant to be
easy to leave. All three routes are on by default:
closeOnOutsideClick— clicking the backdrop closes it. Passfalseto require an explicit choice.hideCloseButton— passtrueto drop the corner button, which is otherwise rendered witharia-label="Close".Escape— native<dialog>behaviour.
Keyboard#
| Key | Behaviour |
|---|---|
| EnterSpace | Opens the drawer when focus is on DrawerTrigger. |
| Escape | Closes the drawer. Native <dialog> behaviour. |
| TabShift+Tab | Cycles focus within the drawer and cannot leave it. |
Accessibility#
DrawerTitlesupplies the accessible name througharia-labelledbyand is required — without it, screen reader users get an unnamed modal.DrawerDescription, when present, is wired up witharia-describedby.- Focus moves into the drawer on open and returns to the trigger on close.
- The page behind is inert via the platform, so it cannot be reached by keyboard or virtual cursor while the drawer is open.
- The slide transition is dropped entirely under
prefers-reduced-motion(WCAG 2.3.3); the drawer simply appears.
A drawer is still a modal
It looks lighter than a dialog, but it traps focus and inerts the page just
the same. For something the user should be able to work alongside, reach for
Popover instead.
API reference#
Drawer#
| Prop | Type | Default |
|---|---|---|
children | ReactNode | — |
defaultOpenInitial open state when uncontrolled. Defaults to `false`. | boolean | — |
onOpenChangeCalled whenever the open state changes, whether from `DialogTrigger`, `DialogClose`, the built-in close button, Escape, or an outside click. | ((open: boolean) => void) | — |
openControls the open state. Omit to let the dialog manage its own state. | boolean | — |
DrawerContent#
| Prop | Type | Default |
|---|---|---|
defaultOpenInitial open state when uncontrolled. Defaults to `false`. | boolean | — |
onOpenChangeCalled whenever the open state changes, whether from `DialogTrigger`, `DialogClose`, the built-in close button, Escape, or an outside click. | ((open: boolean) => void) | — |
openControls the open state. Omit to let the dialog manage its own state. | boolean | — |