# Sheet

> A large, rounded dialog for one task, with a big heading, an optional preview tray, calm rows, and its actions placed by role.

- Name: `sheet`
- Status: beta
- Categories: overlays
- File: the install writes `sheet.tsx` to your `ui` alias, `@/components/ui` by default.
- npm packages: `@base-ui/react`, `class-variance-authority`. The install adds them.
- Other registry items: `@arbokui/action-keys`, `@arbokui/button`. The install adds them.
- Gallery: https://arbokui.com/components/sheet
- Registry item: https://arbokui.com/r/sheet.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the component:

   ```bash
   npx shadcn@latest add @arbokui/sheet
   ```

## Use it for

- One task that needs a few fields or a preview, like inviting members or sharing an event.
- Keep it rendered and set open. false plays the close, then removes it. {show && \<Sheet />} works too, but closes at once.
- onClose runs on Esc and on a press on the backdrop. Set open to false there. The sheet never closes itself.
- Pass actions by role. main is the primary, on the right, with its icon after the label. Give it tone="brand" for the primary look. tertiary, like Cancel or Not now, is a ghost just left of it. secondary is a ghost on the left.
- footer is for what is not an action, like "3 of 40 seats used". It sits on the left.
- tray shows a preview across the top, like the card being shared. avatar puts a 32px photo left of the heading.
- Fields inside keep their own size. Pass size to them as anywhere else.
- inline draws a still picture of it in the page, with no backdrop, for docs and cards.

## Avoid it for

- A yes or no question. Use AlertDialog.
- A short form with plain actions. Use Dialog.
- Content people read beside the page. A sheet hides the page behind it.
- A sheet over a sheet. Finish one task, then open the next.

## Props

- `title`: `ReactNode`. Required. The heading. It names the sheet.
- `subtitle`: `ReactNode`. A quiet line under the heading. It describes the sheet.
- `avatar`: `ReactNode`. Who the sheet is about, like their photo. It sits left of the heading at 32px, cropped round.
- `tray`: `ReactNode`. A preview on a muted tray across the top.
- `children`: `ReactNode`. The rows.
- `actions`: `{ main?: SheetAction; tertiary?: SheetAction; secondary?: SheetAction }`. The actions by role. Each SheetAction is { label, onClick?, icon?, tone?, loading?, disabled?, type?, form? }.
- `SheetAction.tone`: `"neutral" | "brand" | "danger" | "success" | "warning"`. Default `"neutral"`. The main action's color role. Pass "brand" for the primary look. tertiary and secondary stay neutral ghosts.
- `SheetAction.type`: `"button" | "submit"`. Default `"button"`. "submit" sends the form named by form.
- `footer`: `ReactNode`. A note or a status, on the left of the actions.
- `size`: `"sm" | "md" | "lg"`. Default `"md"`. How wide it can grow: sm is 24rem, md is 35rem, lg is 42rem. It never gets wider than the screen.
- `inline`: `boolean`. Default `false`. Draws it in the page's flow, with no backdrop and no focus trap.
- `open`: `boolean`. Default `true`. false plays the close, then removes it.
- `onClose`: `() => void`. Runs on Esc and on a press on the backdrop.
- `className`: `string`. Extra classes on the sheet. Later classes win, so className='max-w-xl' replaces the width.

## Accessibility

- Over the page it is a modal dialog named by its title and described by its subtitle.
- Focus moves onto the sheet itself when it opens, so its title is read first. Tab stays inside, even when pressed fast.
- The page behind is hidden from screen readers and does not scroll.
- Esc closes only what is on top. A picker, menu, or SmartPick inside takes Esc first.
- Focus goes back to what opened it when it closes.
- There is no X button. Give it a tertiary action like Cancel, or people on touch screens have only the backdrop to leave by.
- inline is a section named by its title, with no focus trap.

## Examples

### Invite members

```tsx
"use client";

import { Send } from "lucide-react";
import { useState } from "react";

import { Button } from "@/components/ui/button";

function InviteMembers() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <Button onClick={() => setOpen(true)}>Invite members</Button>
      <Sheet
        open={open}
        onClose={() => setOpen(false)}
        title="Invite members"
        subtitle="They get an email with a link to join the workspace."
        actions={{
          main: { label: "Send invites", icon: <Send />, tone: "brand", onClick: () => setOpen(false) },
          tertiary: { label: "Cancel", onClick: () => setOpen(false) },
        }}
      >
        <p>Add their email addresses.</p>
      </Sheet>
    </>
  );
}
```

### Send a form

```tsx
<Sheet
  open={open}
  onClose={close}
  title="Edit event"
  actions={{ main: { label: "Save", tone: "brand", type: "submit", form: "event" }, tertiary: { label: "Cancel", onClick: close } }}
>
  <form id="event" onSubmit={save}>…</form>
</Sheet>
```

### A still picture, with a preview

```tsx
<Sheet inline title="Share the event" tray={<Card className="w-56 p-4">Design review</Card>} actions={{ main: { label: "Share", tone: "brand" } }} />
```
