Sheet
PortedA panel that slides in from an edge of the screen over a backdrop, such as a record's details beside the list it came from. Put a SheetHeader (the title and a close button), a SheetBody that scrolls, and SheetActions in it. Focus stays inside while it is open and returns to the trigger when it closes. On touch screens, a swipe toward its edge closes it. Native <div> attributes and ref pass through.
import { Button } from "@etched/monet/button";
import { DescriptionList } from "@etched/monet/description-list";
import { Sheet, SheetBody, SheetDescription, SheetHeader, SheetRoot, SheetTrigger } from "@etched/monet/sheet";
export function Preview() {
return (
<SheetRoot>
<SheetTrigger render={<Button>Open candidate</Button>} />
<Sheet size="base">
<SheetHeader close>Ana Ruiz</SheetHeader>
<SheetDescription>Hardware engineer, applied 12 October</SheetDescription>
<SheetBody>
<DescriptionList>
<dt>Stage</dt>
<dd>Phone screen</dd>
<dt>Owner</dt>
<dd>Sam Lee</dd>
</DescriptionList>
</SheetBody>
</Sheet>
</SheetRoot>
);
}Installation
import { Sheet, SheetActions, SheetBody, SheetClose, SheetDescription, SheetHeader, SheetRoot, SheetTitle, SheetTrigger } from "@etched/monet/sheet";Examples
Record details
A candidate's record opens beside the pipeline. The header holds the title and the close button, the body scrolls, and the actions stay at the bottom. Delete asks first, in a dialog over the sheet; the sheet steps back under the shade.
import { Button } from "@etched/monet/button";
import { DescriptionList } from "@etched/monet/description-list";
import { Dialog, DialogActions, DialogClose, DialogDescription, DialogRoot, DialogTitle, DialogTrigger } from "@etched/monet/dialog";
import { Text } from "@etched/monet/text";
import { Sheet, SheetActions, SheetBody, SheetClose, SheetDescription, SheetHeader, SheetRoot, SheetTrigger } from "@etched/monet/sheet";
const notes = [
["Sam Lee, 14 Oct", "Strong on clock trees; led the clock rework on the last tape-out. Wants to stay close to silicon bring-up."],
["Kai Ito, 15 Oct", "Phone screen went well. Asked good questions about our test floor and the board bring-up plan."],
["Ana Ruiz, 16 Oct", "Can start in January. Prefers the Santa Clara lab; open to two days a week in San Jose."],
["Sam Lee, 18 Oct", "References from her last two managers are in. Both mention calm under schedule pressure."],
["Kai Ito, 20 Oct", "Onsite set for 28 Oct: design review, bring-up debugging, and lunch with the team."],
["Sam Lee, 21 Oct", "Sent her the take-home on power integrity. She asked for the board files, which we shared."],
["Ana Ruiz, 22 Oct", "Returned the take-home early, with a short note on a decoupling issue we had missed."],
["Kai Ito, 23 Oct", "The decoupling note checks out. Adding the board team's lead to the onsite panel."],
["Sam Lee, 24 Oct", "Travel booked. She lands on 27 Oct and stays near the Santa Clara lab."],
];
export function RecordDetails() {
return (
<SheetRoot>
<SheetTrigger render={<Button variant="primary">View Ana Ruiz</Button>} />
<Sheet>
<SheetHeader close>Ana Ruiz</SheetHeader>
<SheetDescription>Hardware engineer, applied 12 October</SheetDescription>
<SheetBody>
<DescriptionList layout="columns">
<div>
<dt>Stage</dt>
<dd>Onsite</dd>
</div>
<div>
<dt>Owner</dt>
<dd>Sam Lee</dd>
</div>
<div>
<dt>Email</dt>
<dd>ana.ruiz@example.com</dd>
</div>
<div>
<dt>Location</dt>
<dd>Santa Clara</dd>
</div>
</DescriptionList>
<Text variant="heading3" as="h3">
Notes
</Text>
{notes.map(([byline, note]) => (
<div key={byline}>
<Text variant="secondary" size="sm">
{byline}
</Text>
<Text>{note}</Text>
</div>
))}
</SheetBody>
<SheetActions>
<DialogRoot role="alertdialog">
<DialogTrigger render={<Button variant="secondary-destructive">Delete</Button>} />
<Dialog size="sm">
<DialogTitle>Delete Ana Ruiz?</DialogTitle>
<DialogDescription>Her notes and files are deleted too. You cannot undo this.</DialogDescription>
<DialogActions>
<DialogClose render={<Button>Cancel</Button>} />
<DialogClose render={<Button variant="destructive">Delete</Button>} />
</DialogActions>
</Dialog>
</DialogRoot>
<SheetClose render={<Button variant="primary">Done</Button>} />
</SheetActions>
</Sheet>
</SheetRoot>
);
}Sides and sizes
side on the root picks the edge: end (the default), start or bottom. size sets the width of a start or end sheet on the dialog's scale: base, lg (the default) or xl.
import { Button } from "@etched/monet/button";
import { Sheet, SheetDescription, SheetHeader, SheetRoot, type SheetSide, SheetTrigger } from "@etched/monet/sheet";
const sides: [SheetSide, string][] = [
["start", "From the start"],
["end", "From the end"],
["bottom", "From the bottom"],
];
export function SidesAndSizes() {
return (
<div className="row">
{sides.map(([side, label]) => (
<SheetRoot key={side} side={side}>
<SheetTrigger render={<Button>{label}</Button>} />
<Sheet size={side === "start" ? "base" : "xl"}>
<SheetHeader close>Run 1,284</SheetHeader>
<SheetDescription>Thermal soak, lot 7801. Passed in 42 minutes.</SheetDescription>
</Sheet>
</SheetRoot>
))}
</div>
);
}Saving
While it saves, dismissDisabled keeps the sheet open: Escape, a click outside, a swipe, the close button and Cancel do nothing, and the close buttons show disabled.
import { useState } from "react";
import { Button } from "@etched/monet/button";
import { Input, InputArea } from "@etched/monet/input";
import { Sheet, SheetActions, SheetBody, SheetClose, SheetHeader, SheetRoot, SheetTrigger } from "@etched/monet/sheet";
export function Saving() {
const [open, setOpen] = useState(false);
const [busy, setBusy] = useState(false);
const save = () => {
setBusy(true);
setTimeout(() => {
setBusy(false);
setOpen(false);
}, 1200);
};
return (
<SheetRoot open={open} onOpenChange={setOpen} dismissDisabled={busy}>
<SheetTrigger render={<Button>Edit chip SH-24A-0193</Button>} />
<Sheet>
<SheetHeader close>Edit chip SH-24A-0193</SheetHeader>
<SheetBody>
<Input label="Name" defaultValue="SH-24A-0193" />
<Input label="Test site" defaultValue="Santa Clara lab 2" />
<InputArea label="Notes" rows={3} defaultValue="Runs hot above 1.9 GHz." />
</SheetBody>
<SheetActions>
<SheetClose render={<Button>Cancel</Button>} />
<Button variant="primary" loading={busy} onClick={save}>
Save
</Button>
</SheetActions>
</Sheet>
</SheetRoot>
);
}Accessibility
Escapecloses it, and focus goes back to the trigger. While it is open,TabandShift+Tabstay inside it.- A modal
dialog, named bySheetTitleorSheetHeaderand described bySheetDescription. Withclose, the header has a close button named bycloseLabel("Close"). - While it is open, screen readers do not reach the page behind it. A
SheetBodythat scrolls and holds no control takes focus. - With
dismissDisabled,Escape, a click outside, a swipe and the close buttons do nothing, for example while it saves. - Without a visible title, set
aria-label. To confirm a delete from a sheet, open aDialogRootwithrole="alertdialog".
API reference
Sheet
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "base" | "lg" | "xl" | "lg" | The width of a sheet at the start or the end, on the dialog's scale. |
| initialFocus | PopupProps["initialFocus"] | – | What gets focus when the sheet opens, for example a ref to one field. By default, as in a dialog, the first focusable element. |
| finalFocus | PopupProps["finalFocus"] | – | What gets focus when the sheet closes. By default, the trigger. |
| container | PortalProps["container"] | document.body | The element the sheet renders into, such as a shadow root or a themed region. |
| aria-label | string | – | Names a sheet that has no visible SheetTitle. |
SheetRoot
| Prop | Type | Default | Description |
|---|---|---|---|
| side | "end" | "start" | "bottom" | "end" | The edge the sheet slides in from. A swipe toward that edge closes it. |
| dismissDisabled | boolean | – | While true, the user cannot close the sheet, for example while it saves: Escape, a click outside, a swipe and the close buttons do nothing, and the close buttons show disabled. Closing it from code (open) still works. |