Siteplane UI

Sheet

Open content in an edge panel.

Markdown
"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

pnpm dlx shadcn@4.18.0 add @siteplane/sheet

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

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

Usage

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

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 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.

"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.

"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.

"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 uses this variant for its mobileVariant="fullscreen".

"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

Component properties
PropTypeDefaultDescription
SheetPopup.sideright | left | top | bottomrightPanel edge.
SheetPopup.variantdefault | inset | fullscreendefaultdefault runs from edge to edge, inset floats away from the viewport edges with rounded corners on larger screens, and fullscreen fills the viewport.
SheetPopup.showCloseButtonbooleantrueAutomatic close icon.
SheetPopup.closePropsSheetPrimitive.Close.PropsunsetProps for the automatic close control.
SheetPopup.portalPropsSheetPrimitive.Portal.PropsunsetPortal configuration.
SheetViewport.sideright | left | top | bottomunsetLow-level viewport side; Popup supplies it.
SheetViewport.variantdefault | inset | fullscreendefaultLow-level viewport layout; Popup supplies it.
SheetPanel.scrollFadebooleantrueScroll-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.

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.

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.