Sheet
An edge-anchored panel backed by Radix Dialog — slides in from any side over a scrim, traps focus, and closes on Esc or overlay click. Composed via statics.
Import
import { Sheet, Button } from "@roger-emerson/ploxum-primitives";Usage
Trigger-based (uncontrolled) — wrap your own Button with Sheet.Trigger asChild:
<Sheet>
<Sheet.Trigger asChild>
<Button variant="secondary">Edit profile</Button>
</Sheet.Trigger>
<Sheet.Content side="right" size={480}>
<Sheet.Header>
<Sheet.Title>Edit profile</Sheet.Title>
<Sheet.Description>Make changes and save when you are done.</Sheet.Description>
</Sheet.Header>
<div className="flex flex-1 flex-col gap-4 px-4">
{/* form fields */}
</div>
<Sheet.Footer>
<Sheet.Close asChild>
<Button variant="ghost">Cancel</Button>
</Sheet.Close>
<Sheet.Close asChild>
<Button variant="primary">Save changes</Button>
</Sheet.Close>
</Sheet.Footer>
</Sheet.Content>
</Sheet>Controlled — drive open / onOpenChange yourself (e.g. opening a detail panel from a row click):
const [open, setOpen] = useState(false);
<Sheet onOpenChange={setOpen} open={open}>
<Sheet.Content side="right" size={480}>
<Sheet.Header>
<Sheet.Title>Incident INC-2041</Sheet.Title>
<Sheet.Description>Gate 4 — acknowledged 12:04:21</Sheet.Description>
</Sheet.Header>
{/* detail body */}
</Sheet.Content>
</Sheet>;Props
Sheet (root)
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controlled open state. |
defaultOpen | boolean | — | Uncontrolled initial state. |
onOpenChange | (open: boolean) => void | — | Open-state callback (fires on Esc, overlay click, and Sheet.Close). |
children | ReactNode | — | Trigger (optional) + Content. |
Sheet.Content
| Prop | Type | Default | Description |
|---|---|---|---|
side | "right" | "left" | "top" | "bottom" | "right" | Edge the panel docks to. |
size | number | string | 420 (left/right) · "50vh" (top/bottom) | Width for left/right, height for top/bottom. Numbers are px. Panel is capped at max-w-full. |
hideClose | boolean | false | Hides the built-in top-right close button. |
closeLabel | string | "Close" | aria-label for the close button. |
className | string | — | Merged onto the panel surface. |
Sheet.Trigger / Sheet.Close
Radix Dialog.Trigger / Dialog.Close passthroughs. Use asChild to wrap your own Button. Any number of Sheet.Close elements may sit inside the content (e.g. both Cancel and Save in the footer).
Sheet.Header / Sheet.Footer
Thin <div> wrappers (forward all native div props). Header renders a bottom-bordered title block at the top; Footer renders a top-bordered, right-aligned action bar pinned to the bottom (mt-auto).
Sheet.Title / Sheet.Description
Wrap Radix Dialog.Title / Dialog.Description — they register the accessible name/description for the dialog, so include them (or an equivalent aria-label). Forward all underlying Radix props plus className.
Design rules
- The slide is the one sanctioned motion. Content springs in from the docked edge (
spring, stiffness 300 / damping 32). Underprefers-reduced-motionit degrades to an opacity-only fade — no translation. Don't add secondary entrance flourishes. - The scrim is the one sanctioned backdrop surface. A flat
--ploxum-color-background-scrimoverlay sits behind the panel. Never blur the content behind it — glassmorphism on content surfaces is banned. - Focus is trapped and returns on close. Radix moves focus into the panel on open and restores it to the trigger on close. Esc and overlay click both dismiss.
- Default width is 420px. Override via
sizefor denser detail panels (e.g.480); keep it within one canvas column so the focal map stays legible behind the scrim. - Sheet is for edge-anchored panels;
Dialogis for centered modals. Reach for Sheet when the content is a side rail, drawer, or console (detail drawers, navigation, log consoles). Reach forDialogwhen the interaction is a centered, focused decision.
