<!-- Generated by scripts/gen-docs.mts from the JSDoc in registry/. Do not edit; run `pnpm run docs`. -->

# Sheet

A panel that slides in from an edge and is dragged back to dismiss: the mobile form of a dialog. It stretches when pulled past its edge, its backdrop lightens as it is dragged away, and it springs back on release. It handles focus, Escape, scroll locking, safe areas and the software keyboard.

```tsx
import { Sheet } from "@/components/ouro/sheet/sheet";
```

## Example

```tsx
<Sheet title="Save to cookbook" trigger={<Button>Save</Button>}>
  <CookbookList />
</Sheet>
<Sheet title="Filters" open={open} onOpenChange={setOpen} snapPoints={[0.5, 1]}>
  <FilterForm />
</Sheet>
```

## Usage

Use `trigger` for a sheet a button opens, or `open` with `onOpenChange` to control it.

To close the sheet from an action, control it (`open`/`onOpenChange`) and set `open` to `false` in the handler, or wrap each action in `SheetClose` (`import { SheetClose } from "@/components/ouro/sheet/sheet-parts"`, with your components alias): `<SheetClose render={<Button>Copy link</Button>} />`.

It stacks above the page at `--ouro-layer`, a z-index of 50 by default (from ouro-base.css). If your app has fixed bars above z-50, raise it in a `:root` rule of your own after the Ouro tokens, outside any `@layer`: `:root { --ouro-layer: 60; }`. Don't edit the `--ouro-layer: 50` line shadcn wrote; `shadcn add` resets it.

Testing accessibility: in WebKit, Base UI's focus guards take `role="button"` without a name so VoiceOver's cursor stays trapped. Exclude them: `new AxeBuilder({ page }).exclude("[data-base-ui-focus-guard]")`.

## Use instead when

- Use `Dialog` instead when the content should be a centered dialog on larger screens; it renders this sheet on phones.

## Props

The props the modes share. Type: `SheetProps`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` (required) | `ReactNode` |  | The body. It scrolls when it is taller than the sheet. |
| `className` | `string` |  | Layout only, applied to the sheet panel: width and margins (`max-w-lg`, `mx-auto`). Your classes win over Ouro UI's. Appearance classes warn in development. |
| `closeLabel` | `string` | `"Close"` | The label of the sheet's close button. The button is the last control in the sheet, hidden until it has keyboard focus, so screen-reader users on phones (who have no Escape key) can always close the sheet. |
| `description` | `ReactNode` |  | A short line under the title, read out when the sheet opens. |
| `footer` | `ReactNode` |  | The actions row, stacked full width on phones; primary action last. |
| `hideTitle` | `boolean` |  | Hides the title visually. It still names the dialog for screen readers. |
| `side` | `"top" \| "bottom" \| "left" \| "right"` | `"bottom"` | The edge the sheet slides in from and is dragged back to. |
| `snapPoints` | `SheetSnapPoint[]` |  | Heights a bottom or top sheet rests at, smallest first: fractions of the viewport (`0.5`), `px` or `rem`. `snapPoints={[0.5, 1]}` opens at half height and drags up to full. Without snap points the sheet fits its content. |
| `title` (required) | `string` |  | Names the sheet for screen readers and shows at its top. Always required; hide it with `hideTitle`. |

## Modes

`SheetProps` is a union: pass the props of exactly one mode. Mixing them is a type error.

### Controlled: `SheetControlledProps`

A controlled Sheet: you own `open` and update it in `onOpenChange`.

| Prop | Type | Description |
| --- | --- | --- |
| `onOpenChange` (required) | `(open: boolean) => void` | Called with `false` when the user dismisses the sheet (swipe, Escape, outside press). |
| `open` (required) | `boolean` | Whether the sheet is open. |

Not in this mode (typed `never`):

- `trigger`: Not with `open`. Remove `open` and `onOpenChange` to let a trigger open the sheet.

### With trigger: `SheetWithTriggerProps`

An uncontrolled Sheet: `trigger` opens it.

| Prop | Type | Description |
| --- | --- | --- |
| `trigger` (required) | `ReactElement` | The element that opens the sheet, usually a Button: `trigger={<Button>Save</Button>}`. |

Not in this mode (typed `never`):

- `onOpenChange`: Not with `trigger`. Remove `trigger` to control the sheet with `open` and `onOpenChange`.
- `open`: Not with `trigger`. Remove `trigger` to control the sheet with `open` and `onOpenChange`.

## Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

- `isOpen`: Not a prop. Ouro UI uses `open` with `onOpenChange`: `<Sheet open={open} onOpenChange={setOpen}>`.
- `onClose`: Not a prop. `onOpenChange(false)` fires on close: `<Sheet open={open} onOpenChange={setOpen}>`.
- `position`: Not a prop. Ouro UI uses `side`: `<Sheet side="bottom">`.
- `size`: Not a prop. A sheet fits its content; to set heights use `snapPoints={[0.5, 1]}`.

## `className` is for layout only

Pass margins, width and grid placement (`mt-4`, `w-full`, `col-span-2`).

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (`hover:`, `sm:`) and any `!` are removed, it starts with `bg-`, `text-`, `border-`, `rounded-`, `shadow-`, `font-`, `ring-`, `from-`, `via-`, `to-`, `fill-` or `stroke-`, or is `border`, `rounded`, `shadow` or `ring`, or has a `dark:` variant. Text wrapping, overflow and table layout classes (`text-wrap`, `text-ellipsis`, `border-collapse`) are layout and do not warn.

Alignment classes (`text-center`) warn too: set alignment on a parent element.

## Parts

When these props cannot express your layout, build it from the parts in `@/components/ouro/sheet/sheet-parts`. See [parts.md](parts.md#sheet-parts) for the required nesting.

## Types

### `SheetSnapPoint`

A snap point for a bottom or top sheet: a fraction of the viewport height (`0.5`), or a height in `px` or `rem` (`"320px"`, `"20rem"`).

```ts
type SheetSnapPoint = number | `${number}px` | `${number}rem`;
```
