A modal panel that slides up from the bottom of the screen and can be dragged down to dismiss, for short tasks on phones. Open it with DialogTrigger.
import { Button, DialogTrigger, Drawer } from "@rdloom/react";
export default function DrawerBasicExample() {
return (
<div className="flex w-full justify-center">
<DialogTrigger>
<Button variant="secondary">Share</Button>
<Drawer title="Share this report" description="Anyone with the link can view it.">
{({ close }) => (
<div className="flex flex-col gap-2">
<Button variant="secondary" onPress={close}>
Copy link
</Button>
<Button variant="secondary" onPress={close}>
Send by email
</Button>
<Button variant="ghost" onPress={close}>
Cancel
</Button>
</div>
)}
</Drawer>
</DialogTrigger>
</div>
);
}Installation
npx rdloom add drawerCopies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes. It needs react-aria-components; add --install to install them.
Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/drawer.jsonWorks, but without upgrade tracking.
Usage
import { Button, DialogTrigger, Drawer } from "@rdloom/react";
<div className="flex w-full justify-center">
<DialogTrigger>
<Button variant="secondary">Share</Button>
<Drawer title="Share this report" description="Anyone with the link can view it.">
{({ close }) => (
<div className="flex flex-col gap-2">
<Button variant="secondary" onPress={close}>
Copy link
</Button>
<Button variant="secondary" onPress={close}>
Send by email
</Button>
<Button variant="ghost" onPress={close}>
Cancel
</Button>
</div>
)}
</Drawer>
</DialogTrigger>
</div>With form
import { Button, DialogTrigger, Drawer, TextField } from "@rdloom/react";
export default function DrawerWithFormExample() {
return (
<div className="flex w-full justify-center">
<DialogTrigger>
<Button>Add a note</Button>
<Drawer title="Add a note" description="Notes are visible to your team.">
{({ close }) => (
<form
className="flex flex-col gap-4"
onSubmit={(e) => {
e.preventDefault();
close();
}}
>
<TextField label="Title" name="title" isRequired />
<TextField label="Note" name="note" />
<div className="flex justify-end gap-2">
<Button variant="secondary" onPress={close}>
Cancel
</Button>
<Button type="submit">Save note</Button>
</div>
</form>
)}
</Drawer>
</DialogTrigger>
</div>
);
}Scrollable list
import { Button, DialogTrigger, Drawer } from "@rdloom/react";
const countries = ["Argentina", "Brazil", "Canada", "Chile", "Denmark", "Egypt", "Finland", "Ghana", "Hungary", "India", "Japan", "Kenya", "Latvia", "Mexico", "Norway", "Peru", "Qatar", "Spain"];
export default function DrawerScrollableListExample() {
return (
<div className="flex w-full justify-center">
<DialogTrigger>
<Button variant="secondary">Choose a country</Button>
<Drawer title="Choose a country" description="The drawer stops at 90% of the screen and the list scrolls.">
{({ close }) => (
<ul className="flex flex-col">
{countries.map((name) => (
<li key={name} className="border-b border-[var(--rd-color-border-default)] last:border-b-0">
<button
type="button"
className="w-full py-3 text-start text-sm outline-none focus-visible:ring-2 focus-visible:ring-[var(--rd-color-focus-ring)]"
onClick={close}
>
{name}
</button>
</li>
))}
</ul>
)}
</Drawer>
</DialogTrigger>
</div>
);
}API Reference
Defined by the spec. Components also accept the props of the React Aria component they wrap.
| Prop | Type | Default |
|---|---|---|
titleHeading, also used as the drawer accessible name. Without a title, pass aria-label. | string | none |
descriptionShort supporting text under the title. | string | none |
portalContainerDraw the panel and its backdrop inside this element instead of over the whole page. Use it to show a sheet inside a preview, a framed app or a phone mock. The element must be positioned (relative) and clip its overflow. | Element | null | none |
isDismissableCloses on backdrop click and on a swipe or drag down. Esc always closes. | boolean | true |
childrenBody and actions. A function receives close(). | ReactNode | ((opts: { close: () => void }) => ReactNode) | none |
Accessibility
Role dialog, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.
Keyboard
- Esc closes
- Tab and Shift+Tab stay inside the drawer
Screen readers announce
- Opening announces a dialog with the drawer title
- The description is read after the title
- The drag handle is hidden from assistive technology
- Closing announces the button that opened it (focus is back there)
What your code must do
- Focus moves into the drawer on open and returns to the trigger on close
- Content behind the drawer is hidden from assistive technology
- Page scroll is locked while open
- Dragging is an extra way to close: Esc and any close button in the content always work
- The slide animation and the drag settle are turned off when the user prefers reduced motion
- The drawer is at most 90% of the viewport height and its body scrolls
Guidelines
Use it when
- A short task or a few choices on a phone, within thumb reach
- Actions for one item, such as share or move
Avoid it when
- A side panel on large screens: use Sheet
- A short confirmation: use Dialog or AlertDialog
Don't
- Making dragging the only way to close it: always offer an action button too
- Long multi-step flows inside a drawer
- Drawers opening other drawers
Design tokens
The semantic tokens this component uses. Change them once and every component follows; see Design tokens.
--rd-color-overlay-backdrop--rd-color-surface-raised--rd-color-border-default--rd-color-border-strong--rd-color-text-default--rd-color-text-muted--rd-color-focus-ring--rd-radius-overlay