Sheet
Edge drawer built on the native dialog element.
import { Sheet, SheetTrigger, SheetHeader, SheetTitle, SheetDescription, SheetFooter, SheetClose, SheetContent } from '@elirobinson/react/components/organisms/Sheet';Styles: @elirobinson/react/styles/organisms/Sheet.css — already included when you import @elirobinson/react/styles.css.
Show code
import { useState } from 'react';
import { Button } from '@elirobinson/react/components/atoms/Button';
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@elirobinson/react/components/organisms/Sheet';
export default function Basic() {
const [open, setOpen] = useState(false);
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger className="ds-button ds-button--primary">Filter guides</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Filter coaching guides</SheetTitle>
<SheetDescription>Narrow the list by sport, age group, or session type.</SheetDescription>
</SheetHeader>
<SheetFooter>
<SheetClose />
<Button variant="accent" onClick={() => setOpen(false)}>
Apply filters
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}When to use it
Use Sheet for a task that needs more room than a Popover but shouldn't take over the
whole screen — filters, a detail panel, a settings drawer. It's built on the native
<dialog> element, so the focus trap, the backdrop, and Escape-to-close all come from the
browser instead of a hand-rolled implementation.
Reach for Dialog instead when the content itself is the task and centering it is the
point — a confirmation, a form that should block the rest of the page. A Sheet stays
anchored to an edge, which reads as "secondary to what's behind it" even while it's open.
Sides
SheetContent takes a side — left, right, top, or bottom. right is the default,
shown above.
Show code
import { useState } from 'react';
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@elirobinson/react/components/organisms/Sheet';
const SIDES = ['left', 'right', 'top', 'bottom'] as const;
function SideSheet({ side }: { side: (typeof SIDES)[number] }) {
const [open, setOpen] = useState(false);
const label = side.charAt(0).toUpperCase() + side.slice(1);
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger className="ds-button ds-button--secondary">{label}</SheetTrigger>
<SheetContent side={side}>
<SheetHeader>
<SheetTitle>Opens from the {side}</SheetTitle>
<SheetDescription>
Set side="{side}" on SheetContent to slide in from this edge.
</SheetDescription>
</SheetHeader>
<SheetFooter>
<SheetClose />
</SheetFooter>
</SheetContent>
</Sheet>
);
}
export default function Sides() {
return (
<div className="demo-row">
{SIDES.map((side) => (
<SideSheet key={side} side={side} />
))}
</div>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
defaultOpen | boolean | false | — |
onOpenChange | ((open: boolean) => void) | — | — |
open | boolean | — | — |
SheetTrigger
No props of its own beyond the inherited HTML attributes.
Also accepts all HTMLAttributes<HTMLButtonElement> props.
SheetHeader
No props of its own beyond the inherited HTML attributes.
Also accepts all HTMLAttributes<HTMLDivElement> props.
SheetTitle
No props of its own beyond the inherited HTML attributes.
Also accepts all HTMLAttributes<HTMLHeadingElement> props.
SheetDescription
No props of its own beyond the inherited HTML attributes.
Also accepts all HTMLAttributes<HTMLParagraphElement> props.
SheetFooter
No props of its own beyond the inherited HTML attributes.
Also accepts all HTMLAttributes<HTMLDivElement> props.
SheetClose
No props of its own beyond the inherited HTML attributes.
Also accepts all HTMLAttributes<HTMLButtonElement> props.
SheetContent
| Prop | Type | Default | Description |
|---|---|---|---|
side | "left" | "right" | "top" | "bottom" | right | — |
Also accepts all HTMLAttributes<HTMLDialogElement> props.
Accessibility
SheetContentcalls the nativeshowModal()when it opens, so it inherits the browser's own focus trap,::backdrop, and background inerting — none of that is reimplemented.- Keyboard:
Escapecloses the sheet. That's native<dialog>behavior (it fires acancelthencloseevent), whichSheetlistens for and turns intoonOpenChange(false)— no keydown handler needed. - Clicking the backdrop — the
<dialog>element itself, outside the inner content — also closes the sheet. SheetTitleandSheetDescriptionsupply the idsSheetContentpointsaria-labelledbyandaria-describedbyat. Always render aSheetTitle, even visually hidden — the reference is unconditional, so a sheet without one is a dialog with no accessible name.SheetCloserenders a real<button type="button">with a "Close" fallback label when you don't pass children.
Do
- Render a SheetTitle inside SheetHeader every time, even if you hide it visually.
- Drive visibility with the open/onOpenChange controlled pair.
- Pick side to match the content — top or bottom for something short and wide, left or right for anything list-like.
- Pair SheetClose with an explicit confirm action in SheetFooter so Cancel and the primary action are both one click away.
Don't
- Mix defaultOpen with the controlled open prop — pick uncontrolled (defaultOpen) or controlled (open/onOpenChange), not both.
- Nest a second Sheet inside the first one's content — close the first before opening another.
- Skip SheetDescription and expect the title alone to explain what the sheet is for.
- Put a primary action only inside the sheet with no way to reach it by keyboard — it inherits full native dialog tabbing, so this is rarely an issue unless you fight it with tabIndex.