Overlay
Drawer
Edge-mounted overlay panel that slides in from left or right. Built on Radix Dialog with focus trapping, scroll lock, backdrop dismiss, and directional slide-in/slide-out animations. Use for temporary contextual panels — unlike Sidebar (in-flow layout), Drawer overlays content and dismisses on outside click.
// Radix — @radix-ui/react-dialog
import { Drawer } from "@shavin/ui";Variants
Two presentation variants — floating is the default. Both slide in from the right.
Two presentation variants — floating is the default. Both slide in from the right.
import { Drawer } from "@shavin/ui";
// Floating (default) — inset card with margin, radius, shadow
<Drawer side="right" title="Filters" open={open} onClose={() => setOpen(false)}>
Content
</Drawer>
// Fixed — flush against the edge
<Drawer side="right" variant="fixed" title="Filters" open={open} onClose={() => setOpen(false)}>
Content
</Drawer>Render sides
Drawers can mount from the left or right edge. Both use the floating variant.
Drawers can mount from the left or right edge. Both use the floating variant.
import { Drawer } from "@shavin/ui";
// Right side (default)
<Drawer side="right" title="Quick settings" open={open} onClose={() => setOpen(false)}>
Content
</Drawer>
// Left side
<Drawer side="left" title="Navigation" open={open} onClose={() => setOpen(false)}>
Content
</Drawer>Utilities
Footer action slot and non-dismissible mode for requiring explicit interaction.
Footer action slot and non-dismissible mode for requiring explicit interaction.
import { Drawer, Button } from "@shavin/ui";
// With footer action
<Drawer
side="right"
size="lg"
title="Inspector"
subtitle="Object properties"
open={open}
onClose={() => setOpen(false)}
footer={<Button variant="solid" tone="accent">Apply changes</Button>}
>
Content
</Drawer>
// Non-dismissible — backdrop click disabled
<Drawer
side="right"
title="Required action"
dismissible={false}
open={open}
onClose={() => setOpen(false)}
footer={<Button variant="solid" tone="accent">Acknowledge</Button>}
>
Content
</Drawer>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| open* | boolean | — | Whether the drawer is open. |
| onClose* | () => void | — | Callback fired when drawer requests to close (Escape, backdrop click, close button). |
| title* | ReactNode | — | Drawer header title (string or custom content like a logo). |
| subtitle | string | — | Optional subtitle line below title. |
| side | "left" | "right" | right | Edge the drawer slides in from. Default "right". |
| variant | "fixed" | "floating" | "floating" | Presentation variant. "floating" = inset card (default), "fixed" = flush edge. |
| size | "sm" | "md" | "lg" | "xl" | md | Panel max-width. sm (384px), md (448px), lg (512px), xl (672px). Default "md". |
| dismissible | boolean | true | Whether clicking the backdrop closes the drawer. Default true. |
| overlayOpacity | number | uses | Backdrop opacity intensity 0–1. Default uses the theme's --overlay-opacity token. |
| children | ReactNode | — | Drawer body content. |
| footer | ReactNode | — | Footer slot rendered at the bottom (e.g. action buttons). |
| className | string | — | Additional className overrides for the panel. |
| headerClassName | string | — | Additional className overrides for the header container. |
| modal | boolean | true | Whether to use modal mode (focus trap + pointer event blocking). Default true. Set false to allow nested popovers/menus. |
* required
Keyboard
| Key | Action |
|---|---|
| Tab | Moves focus to the next focusable element (trapped within the drawer). |
| Shift + Tab | Moves focus to the previous focusable element. |
| Escape | Closes the drawer with slide-out animation and returns focus to the trigger. |
Data attributes
| Attribute | Values |
|---|---|
| [data-state] | "open" | "closed" — on the overlay and content while mounted. |
| [data-side] | "left" | "right" — on the content element, reflects the mounted edge. |
| [data-variant] | "floating" | "fixed" — on the content element, reflects the presentation variant. |
Best Practices
| [Do] | Use Drawer for temporary contextual panels like filters, inspectors, quick settings, or mobile navigation that should overlay content and dismiss on outside interaction. |
| [Don't] | Do not use Drawer for persistent navigation or always-visible panels — use Sidebar (in-flow layout) instead. Drawer is for temporary, dismissible surfaces. |
Drawer vs Sidebar
Use Drawer when
- • Temporary filter panels
- • Contextual inspectors
- • Quick settings that don't need to persist
- • Mobile navigation menus
- • Any panel that should dismiss on outside click
Use Sidebar when
- • Persistent app navigation
- • Always-visible configuration panels
- • Dashboards with fixed layout columns
- • Panels that should push content, not overlay
- • When the panel must reserve layout space