Drawer
A panel that slides in from the edge of the screen with swipe-to-dismiss gestures.
Anatomy
Import and assemble the component:
1import { Drawer } from "@raystack/apsara";23<Drawer side="right">4 <Drawer.Trigger />5 <Drawer.SwipeArea />6 <Drawer.Content side="right">7 <Drawer.Header>8 <Drawer.Title />9 <Drawer.Description />10 </Drawer.Header>11 <Drawer.Body />12 <Drawer.Footer />13 </Drawer.Content>14</Drawer>
The indent is optional. It wraps your app:
1<Drawer.Provider>2 <Drawer.Indent>{/* your app, with its drawers */}</Drawer.Indent>3</Drawer.Provider>
Usage
Pick the edge it enters from, then decide who owns its open state.
Basic
A drawer slides in from the edge named by the side of Drawer.Content and traps focus while open.
1<Drawer side="right">2 <Drawer.Trigger render={<Button />}>Open Drawer</Drawer.Trigger>3 <Drawer.Content>4 <Drawer.Header>5 <Drawer.Title>Drawer Title</Drawer.Title>6 <Drawer.Description>Drawer description goes here</Drawer.Description>7 </Drawer.Header>8 <Drawer.Body>9 <span>Main content of the drawer</span>10 </Drawer.Body>11 </Drawer.Content>12</Drawer>
Positioning
The Drawer can slide in from different sides of the screen. Swipe-to-dismiss is automatically configured based on the side prop.
1<Flex gap={5}>2 <Drawer side="top">3 <Drawer.Trigger render={<Button />}>Top Drawer</Drawer.Trigger>4 <Drawer.Content side="top">5 <Drawer.Header>6 <Drawer.Title>Top Drawer</Drawer.Title>7 <Drawer.Description>Slides in from the Top</Drawer.Description>8 </Drawer.Header>9 <Drawer.Body>Content here</Drawer.Body>10 </Drawer.Content>11 </Drawer>12 <Drawer side="right">13 <Drawer.Trigger render={<Button />}>Right Drawer</Drawer.Trigger>14 <Drawer.Content side="right">
Controlled
Pass open with onOpenChange to own the state. Do this when the drawer has to open from elsewhere on the page, or stay open until a save completes.
1(function ControlledDrawer() {2 const [open, setOpen] = React.useState(false);34 return (5 <Flex align="center" gap={5}>6 <Button variant="outline" onClick={() => setOpen(true)}>7 Open from outside8 </Button>9 <Drawer open={open} onOpenChange={setOpen} side="right">10 <Drawer.Content>11 <Drawer.Header>12 <Drawer.Title>Settings</Drawer.Title>13 </Drawer.Header>14 <Drawer.Body>
Swipe to open
Drawer.SwipeArea is an invisible strip along a screen edge. Users drag from it to open the drawer. It sits on the edge named by side. Pass swipeDirection to change the drag direction that opens it. While the drawer is closed, the strip sits above the page and takes clicks along that edge. It ignores pointer events while the drawer is open.
The example keeps the drawer inside its box. Drawer.Content renders into the box through container, and contain: layout on the box positions the fixed parts inside it. modal={false} keeps the rest of the page usable.
1(function SwipeAreaDemo() {2 const [container, setContainer] = React.useState(null);34 return (5 <div6 ref={setContainer}7 style={{8 position: "relative",9 width: "100%",10 overflow: "hidden",11 contain: "layout",12 border: "1px solid var(--rs-color-border-base-primary)",13 borderRadius: "var(--rs-radius-4)",14 }}
Forms and the phone keyboard
Set enableVirtualKeyboard on Drawer.Content when the drawer has form fields. When an input gets focus and the on-screen keyboard opens, the drawer scrolls to keep the input visible, and a bottom drawer moves up above the keyboard. On touch devices, the drawer then handles taps on its inputs itself, so the page does not jump.
Base UI sets --drawer-keyboard-inset on the drawer-viewport slot while the keyboard is open. Use var(--drawer-keyboard-inset, 0px) to lift a pinned footer above the keyboard.
1<Drawer side="bottom">2 <Drawer.Trigger render={<Button />}>Edit profile</Drawer.Trigger>3 <Drawer.Content side="bottom" enableVirtualKeyboard>4 <Drawer.Header>5 <Drawer.Title>Edit profile</Drawer.Title>6 </Drawer.Header>7 <Drawer.Body>8 <Flex direction="column" gap={3}>9 <Input placeholder="Name" />10 <Input type="email" placeholder="Email" />11 </Flex>12 </Drawer.Body>13 </Drawer.Content>14</Drawer>
Indent
Wrap your app in Drawer.Provider and Drawer.Indent. While any drawer inside the provider is open, the indent scales down and rounds its top corners, and a background layer shows behind it. The indent and its background (the drawer-indent-background slot) get a data-active attribute while a drawer is open. The effect follows the swipe as users drag the drawer closed. When the operating system asks for reduced motion, the indent does not move.
Drawer.Indent is at least as tall as the screen. The effect scales from the top of the indent, so keep your app's scroll container inside it. On a page that scrolls the whole document, the visible content shifts up instead of down. While a drawer is open, position: fixed elements inside the indent scale with it.
The example uses the same box as the swipe area example.
1(function IndentDemo() {2 const [container, setContainer] = React.useState(null);34 return (5 <Drawer.Provider>6 <div7 ref={setContainer}8 style={{9 position: "relative",10 width: "100%",11 overflow: "hidden",12 contain: "layout",13 border: "1px solid var(--rs-color-border-base-primary)",14 borderRadius: "var(--rs-radius-4)",
API Reference
The same header, body and footer shell as Dialog, anchored to an edge.
Root
Groups all parts of the drawer. Its side prop sets the swipe-to-dismiss direction. Drawer.Content has its own side, which places the panel. Set both to the same edge.
Prop
Type
Trigger
Renders the button that opens the drawer.
- Inherits all Base UI Drawer.Trigger props
Close
Renders a button that closes the drawer. Drawer.Content already renders one unless showCloseButton is false.
- Inherits all Base UI Drawer.Close props
Content
Renders the drawer panel that slides in from a screen edge.
Prop
Type
Header
children: React.ReactNode - Content to render inside the headerclassName: string - Additional CSS class name
Title
- Inherits all Base UI Drawer.Title props
Description
- Inherits all Base UI Drawer.Description props
Body
- Inherits all HTML div element props
Footer
- Inherits all HTML div element props
SwipeArea
An invisible edge zone that opens the drawer on swipe. Place it inside Drawer.
Prop
Type
Provider
Tracks whether any drawer inside it is open. It renders no element. Drawer.Indent needs it.
Prop
Type
Indent
Wraps your app content and renders the background layer before it. It scales down while a drawer is open. Place it inside Drawer.Provider.
Prop
Type
Slots
Every rendered part carries a stable data-slot attribute for styling and testing:
| Slot | Element |
|---|---|
drawer-backdrop | The overlay behind the drawer |
drawer-viewport | Viewport wrapper that positions the drawer |
drawer-content | The drawer panel (popup) |
drawer-content-body | Content wrapper inside the panel |
drawer-close | The built-in close button (when showCloseButton) |
drawer-header | The Drawer.Header container |
drawer-title | The Drawer.Title element |
drawer-description | The Drawer.Description element |
drawer-body | The Drawer.Body container |
drawer-footer | The Drawer.Footer container |
drawer-swipe-area | The Drawer.SwipeArea edge zone |
drawer-indent | The Drawer.Indent wrapper |
drawer-indent-background | The background layer that Drawer.Indent renders |
Accessibility
- Uses
role="dialog"witharia-modal="true". - Focus is trapped within the drawer and restored on close.
- Supports dismissal with Escape key and swipe gestures.
- Default
aria-labelis"Drawer". Passaria-labeloraria-labelledbyonDrawer.Contentto give the dialog a meaningful name. - Close button label defaults to
"Close". Override with thecloseLabelprop for localisation or context-specific copy (e.g."Close settings"). - Respects motion preferences: drawer slide motion is enabled only when
prefers-reduced-motion: no-preference.
Changelog
Unreleased
See GitHub Releases for every release.