Skip to content

Drawer

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.

Overlayv0.1.0experimentalWCAG 2.2 AAView spec
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 drawer

Copies 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.json

Works, 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.

PropTypeDefault
title

Heading, also used as the drawer accessible name. Without a title, pass aria-label.

stringnone
description

Short supporting text under the title.

stringnone
portalContainer

Draw 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 | nullnone
isDismissable

Closes on backdrop click and on a swipe or drag down. Esc always closes.

booleantrue
children

Body 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