# Sheet

Open content in an edge panel.

Edit profile sheet

[Complete TSX file](/examples/sheet/default.tsx)

```tsx
"use client";

import { Button } from "@/siteplane/components/button";
import { Field, FieldLabel } from "@/siteplane/components/field";
import { Form } from "@/siteplane/components/form";
import { Input } from "@/siteplane/components/input";
import {
  Sheet,
  SheetClose,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetPanel,
  SheetPopup,
  SheetTitle,
  SheetTrigger,
} from "@/siteplane/components/sheet";
import { useState } from "react";

export default function Example() {
  const [open, setOpen] = useState(false);
  return (
    <Sheet open={open} onOpenChange={setOpen}>
      <SheetTrigger render={<Button variant="outline" />}>
        Edit profile
      </SheetTrigger>
      <SheetPopup>
        <SheetHeader>
          <SheetTitle>Edit profile</SheetTitle>
          <SheetDescription>
            Update how your name appears to your team.
          </SheetDescription>
        </SheetHeader>
        <Form
          className="contents"
          onSubmit={(event) => {
            event.preventDefault();
            setOpen(false);
          }}
        >
          <SheetPanel className="grid gap-4">
            <Field>
              <FieldLabel>Name</FieldLabel>
              <Input name="name" defaultValue="Mara Ellis" required />
            </Field>
            <Field>
              <FieldLabel>Username</FieldLabel>
              <Input name="username" defaultValue="mara.ellis" required />
            </Field>
          </SheetPanel>
          <SheetFooter>
            <SheetClose render={<Button variant="ghost" />}>Cancel</SheetClose>
            <Button type="submit">Save changes</Button>
          </SheetFooter>
        </Form>
      </SheetPopup>
    </Sheet>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/sheet
```

With npm: `npx shadcn@4.18.0 add @siteplane/sheet`. [Registry JSON](/registry/sheet.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/button @siteplane/form @siteplane/input
```

## Usage

```tsx
import { Button } from "@/siteplane/components/button";
import {
  Sheet,
  SheetTrigger,
  SheetPopup,
  SheetHeader,
  SheetTitle,
  SheetDescription,
  SheetPanel,
  SheetFooter,
  SheetClose,
} from "@/siteplane/components/sheet";

<Sheet>
  <SheetTrigger render={<Button variant="outline" />}>Open</SheetTrigger>
  <SheetPopup>
    <SheetHeader>
      <SheetTitle>Project details</SheetTitle>
      <SheetDescription>Owner, status and due date.</SheetDescription>
    </SheetHeader>
    <SheetPanel>Content</SheetPanel>
    <SheetFooter>
      <SheetClose render={<Button variant="ghost" />}>Close</SheetClose>
    </SheetFooter>
  </SheetPopup>
</Sheet>;
```

## Composition

```text
Sheet
├── SheetTrigger
└── SheetPopup
    ├── SheetHeader
    │   ├── SheetTitle
    │   └── SheetDescription
    ├── SheetPanel
    └── SheetFooter
        └── SheetClose
```

## Side

Set `side` on `SheetPopup` to open the panel from the right (default), left, top or bottom edge. Sheet uses dialog behavior and has no swipe gestures; use [Drawer](/docs/components/drawer/) for swipe dismissal. In a right-to-left page, `left` and `right` are the inline start and end: a left sheet opens on the right and slides in from that edge.

Sheets on each side

[Complete TSX file](/examples/sheet/positions.tsx)

```tsx
"use client";

import { Button } from "@/siteplane/components/button";
import {
  Sheet,
  SheetClose,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetPopup,
  SheetTitle,
  SheetTrigger,
} from "@/siteplane/components/sheet";

const sides = [
  { side: "top", label: "Top" },
  { side: "right", label: "Right" },
  { side: "bottom", label: "Bottom" },
  { side: "left", label: "Left" },
] as const;

export default function Example() {
  return (
    <div className="flex flex-wrap gap-2">
      {sides.map(({ side, label }) => (
        <Sheet key={side}>
          <SheetTrigger render={<Button variant="outline" />}>
            {label}
          </SheetTrigger>
          <SheetPopup side={side}>
            <SheetHeader>
              <SheetTitle>Notifications</SheetTitle>
              <SheetDescription>
                You&apos;re all caught up. New mentions and assignments appear
                here.
              </SheetDescription>
            </SheetHeader>
            <SheetFooter>
              <SheetClose render={<Button />}>Close</SheetClose>
            </SheetFooter>
          </SheetPopup>
        </Sheet>
      ))}
    </div>
  );
}
```

## Scrollable content

`SheetPanel` scrolls long content between the header and footer, which stay in place.

Scrollable release notes

[Complete TSX file](/examples/sheet/scrolling.tsx)

```tsx
"use client";

import { Button } from "@/siteplane/components/button";
import {
  Sheet,
  SheetClose,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetPanel,
  SheetPopup,
  SheetTitle,
  SheetTrigger,
} from "@/siteplane/components/sheet";

const releases = [
  {
    date: "September 18",
    title: "Saved filters",
    text: "Save a filter combination on the project list and reuse it from the sidebar.",
  },
  {
    date: "September 11",
    title: "Faster search",
    text: "Search results in large workspaces now appear as you type.",
  },
  {
    date: "September 4",
    title: "Task templates",
    text: "Start new tasks from templates with a checklist and default assignee.",
  },
  {
    date: "August 28",
    title: "Due date reminders",
    text: "Get a reminder the day before a task you own is due.",
  },
  {
    date: "August 21",
    title: "Bulk editing",
    text: "Select several tasks to change their status, owner or due date at once.",
  },
  {
    date: "August 14",
    title: "Guest access",
    text: "Invite clients as guests who can only see the projects you share.",
  },
  {
    date: "August 7",
    title: "File previews",
    text: "Preview images and PDFs without downloading them first.",
  },
  {
    date: "July 31",
    title: "Weekly summary",
    text: "Workspace owners can turn on a weekly email with progress across projects.",
  },
  {
    date: "July 24",
    title: "Keyboard shortcuts",
    text: "Press ? anywhere to see the shortcuts for the current page.",
  },
  {
    date: "July 17",
    title: "Project archive",
    text: "Archive finished projects to hide them from the project list.",
  },
  {
    date: "July 10",
    title: "Comment reactions",
    text: "React to comments instead of replying to acknowledge them.",
  },
  {
    date: "July 3",
    title: "Time zone support",
    text: "Due dates now follow each member's time zone.",
  },
];

export default function Example() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        What&apos;s new
      </SheetTrigger>
      <SheetPopup>
        <SheetHeader>
          <SheetTitle>What&apos;s new</SheetTitle>
          <SheetDescription>
            Recent improvements to your workspace.
          </SheetDescription>
        </SheetHeader>
        <SheetPanel>
          <ul className="grid gap-5">
            {releases.map((release) => (
              <li key={release.title} className="grid gap-1 text-sm">
                <p className="text-muted-foreground text-xs">{release.date}</p>
                <p className="font-medium">{release.title}</p>
                <p className="text-muted-foreground">{release.text}</p>
              </li>
            ))}
          </ul>
        </SheetPanel>
        <SheetFooter>
          <SheetClose render={<Button />}>Close</SheetClose>
        </SheetFooter>
      </SheetPopup>
    </Sheet>
  );
}
```

## Inset

Set `variant="inset"` to float the sheet away from the viewport edges with rounded corners on larger screens.

Inset invite sheet

[Complete TSX file](/examples/sheet/inset.tsx)

```tsx
"use client";

import { Button } from "@/siteplane/components/button";
import { Field, FieldLabel } from "@/siteplane/components/field";
import { Form } from "@/siteplane/components/form";
import { Input } from "@/siteplane/components/input";
import {
  Sheet,
  SheetClose,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetPanel,
  SheetPopup,
  SheetTitle,
  SheetTrigger,
} from "@/siteplane/components/sheet";
import { useState } from "react";

export default function Example() {
  const [open, setOpen] = useState(false);
  return (
    <Sheet open={open} onOpenChange={setOpen}>
      <SheetTrigger render={<Button variant="outline" />}>
        Invite members
      </SheetTrigger>
      <SheetPopup variant="inset">
        <SheetHeader>
          <SheetTitle>Invite members</SheetTitle>
          <SheetDescription>
            New members can view and edit every project in this workspace.
          </SheetDescription>
        </SheetHeader>
        <Form
          className="contents"
          onSubmit={(event) => {
            event.preventDefault();
            setOpen(false);
          }}
        >
          <SheetPanel>
            <Field>
              <FieldLabel>Email address</FieldLabel>
              <Input
                name="email"
                type="email"
                placeholder="name@example.com"
                required
              />
            </Field>
          </SheetPanel>
          <SheetFooter>
            <SheetClose render={<Button variant="ghost" />}>Cancel</SheetClose>
            <Button type="submit">Send invite</Button>
          </SheetFooter>
        </Form>
      </SheetPopup>
    </Sheet>
  );
}
```

## Fullscreen navigation

Set `variant="fullscreen"` on `SheetPopup` to fill the viewport, for navigation on a small screen. With `side="top"` the sheet fades in and moves a short distance; with `left` or `right` it slides in from that edge, which suits a nested page that opens from a row. Reduced motion removes the movement. Compose `SheetClose` for a Back action; a nested sheet returns focus to its own trigger when it closes. [Sidebar](/docs/components/sidebar/) uses this variant for its `mobileVariant="fullscreen"`.

Fullscreen menu with a nested page

[Complete TSX file](/examples/sheet/fullscreen.tsx)

```tsx
"use client";

import { ChevronLeftIcon, ChevronRightIcon, MenuIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/siteplane/components/button";
import {
  Sheet,
  SheetClose,
  SheetHeader,
  SheetPanel,
  SheetPopup,
  SheetTitle,
  SheetTrigger,
} from "@/siteplane/components/sheet";

const products = [
  { href: "#analytics", label: "Analytics" },
  { href: "#automations", label: "Automations" },
  { href: "#integrations", label: "Integrations" },
];

const pages = [
  { href: "#pricing", label: "Pricing" },
  { href: "#customers", label: "Customers" },
  { href: "#docs", label: "Docs" },
];

function NavigationLink({
  href,
  label,
  onNavigate,
}: {
  href: string;
  label: string;
  onNavigate: () => void;
}) {
  return (
    <li>
      <Button
        align="start"
        onClick={onNavigate}
        profile="navigation-link"
        render={<a href={href} />}
        size="xl"
        variant="text"
        width="full"
      >
        {label}
      </Button>
    </li>
  );
}

export default function Example() {
  const [open, setOpen] = useState(false);
  const [productsOpen, setProductsOpen] = useState(false);
  // A link closes the nested page and the menu together: an open nested
  // sheet does not close with its parent.
  const closeAll = () => {
    setProductsOpen(false);
    setOpen(false);
  };

  return (
    <Sheet open={open} onOpenChange={setOpen}>
      <SheetTrigger render={<Button variant="outline" />}>
        <MenuIcon aria-hidden="true" />
        Menu
      </SheetTrigger>
      <SheetPopup side="top" variant="fullscreen">
        <SheetHeader>
          <SheetTitle>Menu</SheetTitle>
        </SheetHeader>
        <SheetPanel>
          <nav aria-label="Main">
            <ul className="flex flex-col gap-1">
              <li>
                <Sheet open={productsOpen} onOpenChange={setProductsOpen}>
                  <SheetTrigger
                    render={
                      <Button
                        align="between"
                        profile="navigation-link"
                        size="xl"
                        variant="text"
                        width="full"
                      />
                    }
                  >
                    Products
                    <ChevronRightIcon aria-hidden="true" />
                  </SheetTrigger>
                  <SheetPopup
                    showCloseButton={false}
                    side="right"
                    variant="fullscreen"
                  >
                    <SheetHeader className="items-start">
                      <SheetClose
                        render={
                          <Button
                            profile="navigation-link"
                            size="xl"
                            variant="text"
                          />
                        }
                      >
                        <ChevronLeftIcon aria-hidden="true" />
                        Back
                      </SheetClose>
                      <SheetTitle>Products</SheetTitle>
                    </SheetHeader>
                    <SheetPanel>
                      <ul className="flex flex-col gap-1">
                        {products.map((item) => (
                          <NavigationLink
                            key={item.href}
                            onNavigate={closeAll}
                            {...item}
                          />
                        ))}
                      </ul>
                    </SheetPanel>
                  </SheetPopup>
                </Sheet>
              </li>
              {pages.map((item) => (
                <NavigationLink
                  key={item.href}
                  onNavigate={closeAll}
                  {...item}
                />
              ))}
            </ul>
          </nav>
        </SheetPanel>
      </SheetPopup>
    </Sheet>
  );
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| SheetPopup.side | right \| left \| top \| bottom | right | Panel edge. |
| SheetPopup.variant | default \| inset \| fullscreen | default | default runs from edge to edge, inset floats away from the viewport edges with rounded corners on larger screens, and fullscreen fills the viewport. |
| SheetPopup.showCloseButton | boolean | true | Automatic close icon. |
| SheetPopup.closeProps | SheetPrimitive.Close.Props | unset | Props for the automatic close control. |
| SheetPopup.portalProps | SheetPrimitive.Portal.Props | unset | Portal configuration. |
| SheetViewport.side | right \| left \| top \| bottom | unset | Low-level viewport side; Popup supplies it. |
| SheetViewport.variant | default \| inset \| fullscreen | default | Low-level viewport layout; Popup supplies it. |
| SheetPanel.scrollFade | boolean | true | Scroll-area edge fade. |

`Sheet` and `SheetPortal` are direct Dialog foundation exports. Trigger/Close, Backdrop/Viewport and Title/Description wrap those parts. SheetOverlay aliases Backdrop; SheetContent aliases Popup; SheetPrimitive exports the Dialog namespace. Header/Footer/Panel accept useRender div props (including render, string className and refs). Popup automatically includes Portal, Backdrop, Viewport and portal scope. Other open, modal, focus, dismissal and events follow [Base UI Dialog](https://base-ui.com/react/components/dialog).

`open`/`defaultOpen` choose controlled/initial state (closed by default); `onOpenChange(open, eventDetails)` requests a transition and `onOpenChangeComplete(open)` reports motion completion. Initial/final focus and dismissal behavior belong to the underlying foundation.

`SheetBackdrop` accepts SheetPrimitive.Backdrop props and string className and renders the kit’s fixed full-screen scrim. SheetPopup already renders its backdrop; do not add a duplicate.

[Read the installed source](/source/sheet).

## Usage notes

Provide a title, clear close action and meaningful focus target. Escape and focus restoration follow the foundation. Application loading, errors and async confirmation must be represented explicitly in the content.
