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";
2
3<Drawer side="right">
4 <Drawer.Trigger />
5 <Drawer.SwipeArea />
6 <Drawer.Content>
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 parts are optional. They wrap your app:

1<Drawer.Provider>
2 <Drawer.IndentBackground />
3 <Drawer.Indent>{/* your app, with its drawers */}</Drawer.Indent>
4</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 side 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);
3
4 return (
5 <Flex align="center" gap={5}>
6 <Button variant="outline" onClick={() => setOpen(true)}>
7 Open from outside
8 </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. The swipe area ignores pointer events while the drawer is open.

1<Drawer side="right">
2 <Drawer.Trigger render={<Button />}>Open Drawer</Drawer.Trigger>
3 <Drawer.SwipeArea />
4 <Drawer.Content>
5 <Drawer.Header>
6 <Drawer.Title>Library</Drawer.Title>
7 <Drawer.Description>
8 You can also swipe in from the right edge of the screen.
9 </Drawer.Description>
10 </Drawer.Header>
11 <Drawer.Body>Content here</Drawer.Body>
12 </Drawer.Content>
13</Drawer>

Forms and the phone keyboard

Drawer.Content handles the on-screen keyboard on phones. When an input inside the drawer gets focus and the keyboard opens, the drawer scrolls to keep the input visible. You do not need to set anything up. Base UI also 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">
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, then put Drawer.IndentBackground and Drawer.Indent at the top of the tree. While any drawer inside the provider is open, the indent scales down and rounds its top corners, and the background shows behind it. Both parts get a data-active attribute while a drawer is open. The effect follows the swipe as users drag the drawer closed. With reduced motion, the indent does not move.

1<Drawer.Provider>
2 <Drawer.IndentBackground />
3 <Drawer.Indent>
4 <Drawer side="bottom">
5 <Drawer.Trigger render={<Button />}>Open Drawer</Drawer.Trigger>
6 <Drawer.Content side="bottom">
7 <Drawer.Header>
8 <Drawer.Title>Notifications</Drawer.Title>
9 <Drawer.Description>
10 The content behind this drawer scales down.
11 </Drawer.Description>
12 </Drawer.Header>
13 </Drawer.Content>
14 </Drawer>

API Reference

The same header, body and footer shell as Dialog, anchored to an edge.

Root

Groups all parts of the drawer. The side prop determines both the slide direction and the swipe-to-dismiss direction.

Prop

Type

Content

Renders the drawer panel that slides in from a screen edge.

Prop

Type

  • children: React.ReactNode - Content to render inside the header
  • className: 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
  • 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. You only need it for the indent parts.

Prop

Type

Indent

Wraps your app content. It scales down while a drawer is open.

Prop

Type

IndentBackground

The layer behind Drawer.Indent. It fades in while a drawer is open.

Prop

Type

Slots

Every rendered part carries a stable data-slot attribute for styling and testing:

SlotElement
drawer-backdropThe overlay behind the drawer
drawer-viewportViewport wrapper that positions the drawer
drawer-contentThe drawer panel (popup)
drawer-content-bodyContent wrapper inside the panel
drawer-closeThe built-in close button (when showCloseButton)
drawer-headerThe Drawer.Header container
drawer-titleThe Drawer.Title element
drawer-descriptionThe Drawer.Description element
drawer-bodyThe Drawer.Body container
drawer-footerThe Drawer.Footer container
drawer-swipe-areaThe Drawer.SwipeArea edge zone
drawer-indentThe Drawer.Indent wrapper
drawer-indent-backgroundThe Drawer.IndentBackground layer

Accessibility

  • Uses role="dialog" with aria-modal="true".
  • Focus is trapped within the drawer and restored on close.
  • Supports dismissal with Escape key and swipe gestures.
  • Default aria-label is "Drawer". Pass aria-label or aria-labelledby on Drawer.Content to give the dialog a meaningful name.
  • Close button label defaults to "Close". Override with the closeLabel prop 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.