Skip to content

Sheet

A modal panel that slides in from the edge of the screen, for filters, details or navigation on small screens. Open it with DialogTrigger.

Overlayv0.1.0experimentalWCAG 2.2 AAView spec
import { Button, Checkbox, DialogTrigger, Sheet, Slider } from "@rdloom/react";

export default function SheetFiltersExample() {
  return (
    <DialogTrigger>
      <Button variant="secondary">Filters</Button>
      <Sheet title="Filters" description="Narrow down the orders list.">
        {({ close }) => (
          <div className="flex flex-col gap-6">
            <fieldset className="flex flex-col gap-2">
              <legend className="pb-2 text-sm font-medium">Status</legend>
              <Checkbox defaultSelected>Paid</Checkbox>
              <Checkbox>Shipped</Checkbox>
              <Checkbox>Refunded</Checkbox>
            </fieldset>
            <Slider label="Total" defaultValue={[50, 500]} maxValue={1000} step={10} formatOptions={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }} />
            <div className="flex justify-end gap-2">
              <Button variant="secondary" onPress={close}>
                Cancel
              </Button>
              <Button onPress={close}>Apply</Button>
            </div>
          </div>
        )}
      </Sheet>
    </DialogTrigger>
  );
}

Installation

npx rdloom add sheet

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/sheet.json

Works, but without upgrade tracking.

Usage

import { Button, Checkbox, DialogTrigger, Sheet, Slider } from "@rdloom/react";

<DialogTrigger>
  <Button variant="secondary">Filters</Button>
  <Sheet title="Filters" description="Narrow down the orders list.">
    {({ close }) => (
      <div className="flex flex-col gap-6">
        <fieldset className="flex flex-col gap-2">
          <legend className="pb-2 text-sm font-medium">Status</legend>
          <Checkbox defaultSelected>Paid</Checkbox>
          <Checkbox>Shipped</Checkbox>
          <Checkbox>Refunded</Checkbox>
        </fieldset>
        <Slider label="Total" defaultValue={[50, 500]} maxValue={1000} step={10} formatOptions={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }} />
        <div className="flex justify-end gap-2">
          <Button variant="secondary" onPress={close}>
            Cancel
          </Button>
          <Button onPress={close}>Apply</Button>
        </div>
      </div>
    )}
  </Sheet>
</DialogTrigger>

Navigation

import { Button, DialogTrigger, Sheet } from "@rdloom/react";

const links = ["Dashboard", "Orders", "Customers", "Reports", "Settings"];

export default function SheetNavigationExample() {
  return (
    <DialogTrigger>
      <Button variant="secondary">Menu</Button>
      {/* start: the left edge, or the right in right-to-left languages. */}
      <Sheet title="Navigation" side="start" size="sm">
        <nav aria-label="Main">
          <ul className="flex flex-col gap-1">
            {links.map((l) => (
              <li key={l}>
                <a href={`#${l.toLowerCase()}`} className="block rounded-md px-3 py-2 text-sm hover:bg-[var(--rd-color-surface-subtle)]">
                  {l}
                </a>
              </li>
            ))}
          </ul>
        </nav>
      </Sheet>
    </DialogTrigger>
  );
}

Bottom

import { Button, DialogTrigger, Sheet } from "@rdloom/react";

export default function SheetBottomExample() {
  return (
    <DialogTrigger>
      <Button variant="secondary">Share</Button>
      {/* Bottom sheets suit phones: the content is within thumb reach. */}
      <Sheet title="Share" description="Anyone with the link can view." side="bottom" size="sm">
        {({ close }) => (
          <div className="flex flex-wrap gap-2">
            <Button variant="secondary" onPress={close}>
              Copy link
            </Button>
            <Button variant="secondary" onPress={close}>
              Email
            </Button>
            <Button variant="secondary" onPress={close}>
              Embed
            </Button>
          </div>
        )}
      </Sheet>
    </DialogTrigger>
  );
}

API Reference

Defined by the spec. Components also accept the props of the React Aria component they wrap.

PropTypeDefault
titlerequired

Heading, also used as the sheet's accessible name.

stringnone
description

Short supporting text under the title.

stringnone
side

The edge it slides in from. end is the right in left-to-right languages.

"end" | "start" | "bottom""end"
size

Width (or height for bottom sheets).

"sm" | "md" | "lg""md"
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. 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 sheet
  • The close button is reachable with Tab

Screen readers announce

  • Opening announces a dialog with the sheet's title
  • The description is read after the title
  • The close button is announced as Close
  • Closing announces the button that opened it (focus is back there)

What your code must do

  • Focus moves into the sheet on open and returns to the trigger on close
  • Content behind the sheet is hidden from assistive technology
  • Page scroll is locked while open
  • The slide animation is turned off when the user prefers reduced motion

Guidelines

Use it when

  • Filters or settings next to the content they affect
  • Details of a row without leaving the list
  • Navigation on phones

Avoid it when

  • A short confirmation: use Dialog
  • Content people need side by side with the page: use a layout panel

Don't

  • Sheets opening other sheets
  • Hiding the only way to close it: keep the close button

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-text-default
  • --rd-color-text-muted
  • --rd-color-surface-subtle
  • --rd-color-focus-ring
  • --rd-radius-overlay