A modal panel that slides in from the edge of the screen, for filters, details or navigation on small screens. Open it with DialogTrigger.
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 sheetCopies 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.jsonWorks, 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.
| Prop | Type | Default |
|---|---|---|
titlerequiredHeading, also used as the sheet's accessible name. | string | none |
descriptionShort supporting text under the title. | string | none |
sideThe edge it slides in from. end is the right in left-to-right languages. | "end" | "start" | "bottom" | "end" |
sizeWidth (or height for bottom sheets). | "sm" | "md" | "lg" | "md" |
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. 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 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