Monet
@etched/monet 0.0.1

Sheet

Ported

A 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

  • Escape closes it, and focus goes back to the trigger. While it is open, Tab and Shift+Tab stay inside it.
  • A modal dialog, named by SheetTitle or SheetHeader and described by SheetDescription. With close, the header has a close button named by closeLabel ("Close").
  • While it is open, screen readers do not reach the page behind it. A SheetBody that 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 a DialogRoot with role="alertdialog".

API reference

Sheet

PropTypeDefaultDescription
size"base" | "lg" | "xl""lg"The width of a sheet at the start or the end, on the dialog's scale.
initialFocusPopupProps["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.
finalFocusPopupProps["finalFocus"]–What gets focus when the sheet closes. By default, the trigger.
containerPortalProps["container"]document.bodyThe element the sheet renders into, such as a shadow root or a themed region.
aria-labelstring–Names a sheet that has no visible SheetTitle.

SheetRoot

PropTypeDefaultDescription
side"end" | "start" | "bottom""end"The edge the sheet slides in from. A swipe toward that edge closes it.
dismissDisabledboolean–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.