Sheet

A slide-in panel that appears from the left or right edge of the viewport. Can operate as a modal dialog with a backdrop or as a popover that allows interaction with content behind it. Supports a title, description, action buttons, and optional form wrapping. Controlled via an imperative ref with open() and close() methods.

Sheet panel

This is a slide-in panel.

Sheet content goes here.
import { Button } from "@tenstorrent/vesper/button";
import { Sheet, useSheet } from "@tenstorrent/vesper/sheet";

export default function SheetDemo() {
  const sheet = useSheet();

  return (
    <>
      <Button onClick={sheet.open}>Open sheet</Button>
      <Sheet
        ref={sheet.ref}
        title="Sheet panel"
        description="This is a slide-in panel."
        buttons={[
          { children: "Cancel", onClick: sheet.close },
          { children: "Save", onClick: sheet.close },
        ]}
      >
        Sheet content goes here.
      </Sheet>
    </>
  );
}

Options

PropTypeDescriptionDefault
titlestringThe title text displayed in the sheet header. Used for aria-labelledby. Required.—
descriptionstringA description displayed below the title. Used for aria-describedby. Required.—
side"left" | "right"The side of the viewport the sheet slides in from."right"
popoverbooleanWhen true, renders as a popover without a backdrop, allowing interaction behind it.false
buttonsButtonProps[]An optional array of button props for action buttons. The last button defaults to "contrast" variant; others default to "tertiary".—
refRef<SheetRef>A ref exposing imperative open() and close() methods.—
formFormPropsWhen provided, wraps the sheet content in a <form> element with the given form attributes.—
childrenReactNodeThe content displayed inside the sheet body.—

All other props are forwarded to the underlying <dialog> element.

Use a Sheet for supplementary content alongside the current view, such as filters, settings, or the details of a selected row. When the content demands the user's full attention before they can carry on, reach for the Modal component instead.

Examples

Opening and closing a sheet

A Sheet renders closed, and is opened imperatively. The useSheet hook returns everything needed for this: a ref to forward to the sheet, plus open() and close() functions:

Settings

Manage your preferences.

Sheet content goes here.
import { Button } from "@tenstorrent/vesper/button";
import { Sheet, useSheet } from "@tenstorrent/vesper/sheet";

export default function BasicSheet() {
  const sheet = useSheet();

  return (
    <>
      <Button onClick={sheet.open}>Open settings</Button>
      <Sheet
        ref={sheet.ref}
        title="Settings"
        description="Manage your preferences."
      >
        Sheet content goes here.
      </Sheet>
    </>
  );
}

A sheet closes when the user presses Escape, clicks the close button in its header, or (in the default modal mode) clicks the backdrop behind it.

Choosing a side

A sheet slides in from the right by default. Pass side="left" to have it slide in from the left instead, which tends to suit navigation:

Navigation

Browse sections.

Sheet content goes here.
import { Button } from "@tenstorrent/vesper/button";
import { Sheet, useSheet } from "@tenstorrent/vesper/sheet";

export default function LeftSheet() {
  const sheet = useSheet();

  return (
    <>
      <Button onClick={sheet.open}>Open navigation</Button>
      <Sheet
        ref={sheet.ref}
        side="left"
        title="Navigation"
        description="Browse sections."
      >
        Sheet content goes here.
      </Sheet>
    </>
  );
}

Popover mode

By default a Sheet behaves like a modal dialog: it renders a backdrop, traps focus, and blocks interaction with the rest of the page. Passing popover renders it as a popover instead, with no backdrop, so the user can keep working with the content behind it:

Details

Additional information.

This sheet does not block interaction with the page behind it, so you can keep scrolling and clicking while it is open.
import { Button } from "@tenstorrent/vesper/button";
import { Sheet, useSheet } from "@tenstorrent/vesper/sheet";

export default function PopoverSheet() {
  const sheet = useSheet();

  return (
    <>
      <Button onClick={sheet.open}>Open details</Button>
      <Sheet
        ref={sheet.ref}
        popover
        title="Details"
        description="Additional information."
      >
        This sheet does not block interaction with the page behind it, so you
        can keep scrolling and clicking while it is open.
      </Sheet>
    </>
  );
}

Use popover mode for panels the user consults while working, such as inspectors and help content, and stick with the default modal mode when the panel's task should be finished before anything else happens.

Action buttons

Pass an array of button props to buttons to render action buttons along the bottom of the sheet. The last button defaults to the contrast variant and the rest default to tertiary, which can be overridden with the variant prop:

Edit profile

Update your information.

import { Button } from "@tenstorrent/vesper/button";
import { Sheet, useSheet } from "@tenstorrent/vesper/sheet";
import { TextInput } from "@tenstorrent/vesper/text-input";

export default function SheetWithButtons() {
  const sheet = useSheet();

  return (
    <>
      <Button onClick={sheet.open}>Edit profile</Button>
      <Sheet
        ref={sheet.ref}
        title="Edit profile"
        description="Update your information."
        buttons={[
          { children: "Cancel", onClick: sheet.close },
          { children: "Save changes", onClick: sheet.close },
        ]}
      >
        <TextInput
          aria-label="Display name"
          placeholder="Display name"
          name="name"
        />
      </Sheet>
    </>
  );
}

Usage in forms

Passing the form prop wraps the sheet's contents in a <form> element, so the action buttons can submit it natively with type="submit", and native validation runs before the submit handler fires:

Invite teammate

Send an invitation to join your workspace.

Enter your teammate's email

import { useState } from "react";

import { Button } from "@tenstorrent/vesper/button";
import { Sheet, useSheet } from "@tenstorrent/vesper/sheet";
import { TextInput } from "@tenstorrent/vesper/text-input";
import { Typography } from "@tenstorrent/vesper/typography";

export default function FormSheet() {
  const sheet = useSheet();
  const [submitted, setSubmitted] = useState<string | null>(null);

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        gap: "var(--vesper-spacing-4)",
        alignItems: "flex-start",
      }}
    >
      <Button onClick={sheet.open}>Invite teammate</Button>
      {submitted && (
        <Typography variant="copy-sm">Invited: {submitted}</Typography>
      )}
      <Sheet
        ref={sheet.ref}
        title="Invite teammate"
        description="Send an invitation to join your workspace."
        form={{
          onSubmit: (event) => {
            event.preventDefault();
            const data = new FormData(event.currentTarget);
            setSubmitted(String(data.get("email") ?? ""));
            sheet.close();
          },
        }}
        buttons={[
          { children: "Cancel", type: "button", onClick: sheet.close },
          { children: "Send invite", type: "submit" },
        ]}
      >
        <Typography
          variant="label-sm"
          style={{ marginBottom: "var(--vesper-spacing-2)" }}
        >
          Enter your teammate's email
        </Typography>
        <TextInput
          required
          type="email"
          aria-label="Email address"
          placeholder="Email address"
          name="email"
        />
      </Sheet>
    </div>
  );
}
Buttons rendered inside a form default to type="submit", so give any button that should not submit an explicit type="button", as the cancel button above does.