# Sheet

> Open a side panel for details, settings, or a secondary workflow.

Source: https://www.honestui.com/docs/components/sheet

```tsx
"use client"

import { Button } from "@/components/honest-ui/ui/button"
import { Field, FieldControl, FieldLabel } from "@/components/honest-ui/ui/field"
import { Form } from "@/components/honest-ui/ui/form"
import {
  Sheet,
  SheetClose,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetPopup,
  SheetTitle,
  SheetTrigger,
} from "@/components/honest-ui/ui/sheet"

export function DialogDemo() {
  return (
    <Sheet>
      <SheetTrigger
        render={(props) => (
          <Button {...props} variant="secondary">
            Open Sheet
          </Button>
        )}
      />
      <SheetPopup>
        <Form className="grid gap-4">
          <SheetHeader>
            <SheetTitle>Edit profile</SheetTitle>
            <SheetDescription>
              Make changes to your profile here. Click save when
              you&apos;re done.
            </SheetDescription>
          </SheetHeader>
          <div className="flex flex-col gap-4 px-4">
            <Field>
              <FieldLabel>Name</FieldLabel>
              <FieldControl type="text" defaultValue="Connor Love" />
            </Field>
            <Field>
              <FieldLabel>Username</FieldLabel>
              <FieldControl type="text" defaultValue="@loveconnor" />
            </Field>
          </div>
          <SheetFooter>
            <SheetClose
              render={(props) => (
                <Button {...props} variant="ghost">
                  Cancel
                </Button>
              )}
            />
            <Button type="submit">Save</Button>
          </SheetFooter>
        </Form>
      </SheetPopup>
    </Sheet>
  )
}

```

## Overview [#overview]

Use Sheet for a panel that slides in from the edge of the screen while the current page stays visible behind a dimmed backdrop. Sheets suit details panels, filters, quick edit forms, carts, checkout summaries, and secondary workflows where the person needs context from the page but should not wander off it.

Choose between Sheet and Dialog by shape of attention: a Dialog centers attention on one short decision, while a Sheet keeps a wider working surface anchored to an edge so people can still see what they were doing. If the content is a single question with two buttons, use Dialog or Alert Dialog instead — see [Don't do this](#dont-do-this). If the content deserves its own URL and back-button behavior, give it a route.

## Anatomy [#anatomy]

A sheet has a trigger, popup panel, header, title, description, body content, footer, and a built-in close button. It is built on the Base UI Dialog primitives, so everything about focus, modality, and dismissal behaves like a Dialog — only the position and animation differ.

The header holds the title and description. The body scrolls independently when content exceeds the viewport height. The footer sits at the bottom of the panel regardless of body height, which keeps primary actions reachable without scrolling.

## Sides and sizing [#sides-and-sizing]

The `side` prop on `SheetPopup` accepts `"top"`, `"right"`, `"bottom"`, or `"left"` and defaults to `"right"`.

Right-side sheets work well for details and editing because they start where the eye finishes reading in left-to-right locales. Left-side sheets suit navigation drawers. Bottom sheets work well for compact pickers on small screens. Top sheets are rare; reserve them for notifications or global search.

On phones, right and left sheets span the full width minus a small gutter. From `sm` upward they cap at a comfortable reading width, so a wide screen never produces a panel stretched across it.

## Behavior [#behavior]

**Opening.** When the sheet opens, focus moves into the panel, the backdrop dims the page, and scrolling behind is locked. The slide-in animation respects motion preferences through transition utilities rather than JavaScript timers.

**While open.** <kbd>Tab</kbd> and <kbd>Shift</kbd>+<kbd>Tab</kbd> cycle inside the panel only. <kbd>Escape</kbd> closes it, and clicking the backdrop closes it too. Use the `showCloseButton={false}` prop on `SheetPopup` to remove the built-in dismiss button when you provide your own `SheetClose` control.

**Closing.** Focus returns to the trigger so keyboard users continue where they left off. Because sheets often contain forms, intercept accidental dismissal with controlled state when discarding input would lose work — the same pattern shown in the [Dialog close confirmation](/docs/components/dialog#close-confirmation) example applies unchanged.

## Accessibility [#accessibility]

Always provide a `SheetTitle`; it gives the panel its accessible name, so screen-reader users know what opened. Add `SheetDescription` when the task needs context. Without a title the panel is announced as an unlabeled dialog.

The built-in close button carries a visually hidden "Close" label and grows to at least 44 × 44 px on touch devices, so it stays easy to hit even though it looks small. Keep your own controls inside the panel at least that large when they are primary touch targets.

Place initial focus deliberately with `initialFocus` on `SheetPopup` when the first tabbable element is not the right starting point — for example, focusing the first form field instead of the close button.

Colors come from theme tokens, so the panel and backdrop adapt to dark mode automatically. The close button is positioned with the logical `end` property, so it mirrors correctly in right-to-left locales. The four sides themselves are physical positions, though: `side="right"` stays on the physical right under `dir="rtl"`. Pick the side deliberately for localized layouts rather than assuming mirroring.

Long titles and descriptions wrap inside the panel instead of overflowing it. At 200% zoom the body scrolls while the footer actions stay pinned, so the task remains completable.

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;sheet&#x22;]" />
  

  
    
      
        Install the following dependencies:
      

      ```bash
      npm install @base-ui-components/react
      ```

      
        Copy and paste the following code into your project.
      

      ### components/ui/sheet.tsx

```tsx
"use client"

import { Dialog as SheetPrimitive } from "@base-ui-components/react/dialog"
import { X as XIcon } from "honestui/icons"

import { cn } from "@/lib/utils"

function Sheet(props: SheetPrimitive.Root.Props) {
  return <SheetPrimitive.Root data-slot="sheet" {...props} />
}

function SheetTrigger(props: SheetPrimitive.Trigger.Props) {
  return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />
}

function SheetPortal(props: SheetPrimitive.Portal.Props) {
  return <SheetPrimitive.Portal {...props} />
}

function SheetClose(props: SheetPrimitive.Close.Props) {
  return <SheetPrimitive.Close data-slot="sheet-close" {...props} />
}

function SheetBackdrop({ className, ...props }: SheetPrimitive.Backdrop.Props) {
  return (
    <SheetPrimitive.Backdrop
      data-slot="sheet-backdrop"
      className={cn(
        "fixed inset-0 z-50 bg-black/32 backdrop-blur-sm transition-all duration-200 data-ending-style:opacity-0 data-starting-style:opacity-0",
        className
      )}
      {...props}
    />
  )
}

function SheetPopup({
  className,
  children,
  showCloseButton = true,
  side = "right",
  ...props
}: SheetPrimitive.Popup.Props & {
  showCloseButton?: boolean
  side?: "top" | "right" | "bottom" | "left"
}) {
  return (
    <SheetPortal>
      <SheetBackdrop />
      <SheetPrimitive.Popup
        data-slot="sheet-popup"
        className={cn(
          "fixed z-50 flex h-[100dvh] flex-col gap-4 bg-popover text-popover-foreground shadow-lg transition-[opacity,translate] duration-300 ease-in-out will-change-transform",
          side === "right" &&
            "inset-y-0 right-0 h-full w-[calc(100%-(--spacing(12)))] max-w-sm data-ending-style:translate-x-full data-starting-style:translate-x-full",
          side === "left" &&
            "inset-y-0 left-0 h-full w-[calc(100%-(--spacing(12)))] max-w-sm data-ending-style:-translate-x-full data-starting-style:-translate-x-full",
          side === "top" &&
            "inset-x-0 top-0 h-auto data-ending-style:-translate-y-full data-starting-style:-translate-y-full",
          side === "bottom" &&
            "inset-x-0 bottom-0 h-auto data-ending-style:translate-y-full data-starting-style:translate-y-full",
          className
        )}
        {...props}
      >
        {children}
        {showCloseButton && (
          <SheetPrimitive.Close className="absolute end-2 top-2 inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-72 transition-[color,background-color,box-shadow,opacity] outline-none hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
            <XIcon />
            <span className="sr-only">Close</span>
          </SheetPrimitive.Close>
        )}
      </SheetPrimitive.Popup>
    </SheetPortal>
  )
}

function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="sheet-header"
      className={cn("flex flex-col gap-1.5 p-4", className)}
      {...props}
    />
  )
}

function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="sheet-footer"
      className={cn(
        "mt-auto flex flex-col gap-2 p-4 *:w-full sm:flex-row sm:justify-end sm:*:w-auto",
        className
      )}
      {...props}
    />
  )
}

function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
  return (
    <SheetPrimitive.Title
      data-slot="sheet-title"
      className={cn("font-semibold", className)}
      {...props}
    />
  )
}

function SheetDescription({
  className,
  ...props
}: SheetPrimitive.Description.Props) {
  return (
    <SheetPrimitive.Description
      data-slot="sheet-description"
      className={cn("text-sm text-muted-foreground", className)}
      {...props}
    />
  )
}

export {
  Sheet,
  SheetTrigger,
  SheetPortal,
  SheetClose,
  SheetBackdrop,
  SheetBackdrop as SheetOverlay,
  SheetPopup,
  SheetPopup as SheetContent,
  SheetHeader,
  SheetFooter,
  SheetTitle,
  SheetDescription,
}

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

```tsx
import {
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet";
```

```tsx
<Sheet>
  <SheetTrigger>Edit profile</SheetTrigger>
  <SheetPopup>
    <SheetHeader>
      <SheetTitle>Edit profile</SheetTitle>
      <SheetDescription>
        Changes are visible to other workspace members immediately.
      </SheetDescription>
    </SheetHeader>
    {/* Body fields */}
  </SheetPopup>
</Sheet>
```

`SheetContent` is an alias for `SheetPopup`, and `SheetOverlay` aliases `SheetBackdrop`, so either naming style works.

## Don't do this [#dont-do-this]

### A confirmation belongs in a dialog [#a-confirmation-belongs-in-a-dialog]

```tsx
// Bad
<Sheet>
  <SheetTrigger>Delete workspace</SheetTrigger>
  <SheetPopup>
    <SheetHeader>
      <SheetTitle>Delete workspace?</SheetTitle>
    </SheetHeader>
    <SheetFooter>
      <Button variant="destructive">Delete</Button>
    </SheetFooter>
  </SheetPopup>
</Sheet>
```

```tsx
// Good
<AlertDialog>
  <AlertDialogTrigger>Delete workspace</AlertDialogTrigger>
  <AlertDialogPopup>
    <AlertDialogHeader>
      <AlertDialogTitle>Delete workspace?</AlertDialogTitle>
    </AlertDialogHeader>
    <AlertDialogBody>
      <AlertDialogDescription>
        This permanently deletes the workspace and its data.
      </AlertDialogDescription>
    </AlertDialogBody>
    <AlertDialogFooter>
      <AlertDialogClose>Cancel</AlertDialogClose>
      <AlertDialogClose render={<Button variant="destructive" />}>
        Delete workspace
      </AlertDialogClose>
    </AlertDialogFooter>
  </AlertDialogPopup>
</AlertDialog>
```

A sheet sliding in from the edge signals "working surface," not "stop and decide." Short confirmations land faster in a centered dialog, where the backdrop and compact size make the decision the only thing on screen. Reserve sheets for tasks with real content to read or fill in.

### Panels without a title [#panels-without-a-title]

```tsx
// Bad
<SheetPopup showCloseButton>
  {/* filters */}
</SheetPopup>
```

```tsx
// Good
<SheetPopup>
  <SheetHeader>
    <SheetTitle>Filters</SheetTitle>
  </SheetHeader>
  {/* filters */}
</SheetPopup>
```

The accessible name comes from the title. Without it, assistive technology announces an unlabeled dialog, and people cannot tell what opened or why focus moved. Even a visually minimal panel gets a `SheetTitle` — hide it visually only if the surrounding design truly replaces it.

### Full workflows hidden in a panel [#full-workflows-hidden-in-a-panel]

```tsx
// Bad
<Sheet>
  <SheetTrigger>Checkout</SheetTrigger>
  <SheetPopup>
    {/* Address, payment, review, confirmation — four steps */}
  </SheetPopup>
</Sheet>
```

```tsx
// Good — give multi-step flows a route
[Checkout](/checkout)
```

Sheets tempt you to stack several steps into one panel because opening them is cheap. But a multi-step flow in a panel has no URL to resume or share, browser Back closes the whole flow at once, and reload discards progress mid-panel. Once content scrolls past a screen or represents navigation, it deserves a page.

## Examples [#examples]

### Quick edit form [#quick-edit-form]

Fields stay labeled and the footer actions remain visible while the form area scrolls.

```tsx
import { Button } from "@/components/honest-ui/ui/button"
import { Field, FieldControl, FieldLabel } from "@/components/honest-ui/ui/field"
import { Sheet, SheetFooter, SheetHeader, SheetPopup, SheetTitle, SheetTrigger } from "@/components/honest-ui/ui/sheet"

export function SheetProfileEditor() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="secondary" />}>Edit profile</SheetTrigger>
      <SheetPopup>
        <SheetHeader><SheetTitle>Profile</SheetTitle></SheetHeader>
        <div className="grid gap-4 px-4">
          <Field><FieldLabel>Name</FieldLabel><FieldControl defaultValue="Alex Morgan" /></Field>
          <Field><FieldLabel>Role</FieldLabel><FieldControl defaultValue="Designer" /></Field>
        </div>
        <SheetFooter><Button>Save changes</Button></SheetFooter>
      </SheetPopup>
    </Sheet>
  )
}

```

### Order summary [#order-summary]

A compact summary panel with a single pinned action — the shape carts and checkout overviews take.

```tsx
import { Button } from "@/components/honest-ui/ui/button"
import { Sheet, SheetFooter, SheetHeader, SheetPopup, SheetTitle, SheetTrigger } from "@/components/honest-ui/ui/sheet"

export function SheetCheckoutSummary() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="secondary" />}>Order summary</SheetTrigger>
      <SheetPopup>
        <SheetHeader><SheetTitle>Checkout</SheetTitle></SheetHeader>
        <div className="grid gap-3 px-4 text-sm">
          <div className="flex justify-between"><span>Pro plan</span><span>$29</span></div>
          <div className="flex justify-between"><span>Tax</span><span>$2.32</span></div>
          <div className="flex justify-between font-medium"><span>Total</span><span>$31.32</span></div>
        </div>
        <SheetFooter><Button>Pay now</Button></SheetFooter>
      </SheetPopup>
    </Sheet>
  )
}

```

### Form with description and close [#form-with-description-and-close]

```tsx
"use client"

import { Button } from "@/components/honest-ui/ui/button"
import { Field, FieldControl, FieldLabel } from "@/components/honest-ui/ui/field"
import { Form } from "@/components/honest-ui/ui/form"
import {
  Sheet,
  SheetClose,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetPopup,
  SheetTitle,
  SheetTrigger,
} from "@/components/honest-ui/ui/sheet"

export function DialogDemo() {
  return (
    <Sheet>
      <SheetTrigger
        render={(props) => (
          <Button {...props} variant="secondary">
            Open Sheet
          </Button>
        )}
      />
      <SheetPopup>
        <Form className="grid gap-4">
          <SheetHeader>
            <SheetTitle>Edit profile</SheetTitle>
            <SheetDescription>
              Make changes to your profile here. Click save when
              you&apos;re done.
            </SheetDescription>
          </SheetHeader>
          <div className="flex flex-col gap-4 px-4">
            <Field>
              <FieldLabel>Name</FieldLabel>
              <FieldControl type="text" defaultValue="Connor Love" />
            </Field>
            <Field>
              <FieldLabel>Username</FieldLabel>
              <FieldControl type="text" defaultValue="@loveconnor" />
            </Field>
          </div>
          <SheetFooter>
            <SheetClose
              render={(props) => (
                <Button {...props} variant="ghost">
                  Cancel
                </Button>
              )}
            />
            <Button type="submit">Save</Button>
          </SheetFooter>
        </Form>
      </SheetPopup>
    </Sheet>
  )
}

```

### Side placement [#side-placement]

The same panel opened from each edge, including a top sheet with its built-in close button removed via `showCloseButton={false}`.

```tsx
"use client"

import { Button } from "@/components/honest-ui/ui/button"
import {
  Sheet,
  SheetDescription,
  SheetHeader,
  SheetPopup,
  SheetTitle,
  SheetTrigger,
} from "@/components/honest-ui/ui/sheet"

export function DialogDemo() {
  return (
    <div className="flex flex-wrap gap-2">
      <Sheet>
        <SheetTrigger
          render={(props) => (
            <Button {...props} variant="secondary">
              Open Right
            </Button>
          )}
        />
        <SheetPopup showCloseButton={false}>
          <SheetHeader>
            <SheetTitle>Right</SheetTitle>
            <SheetDescription>
              Right side of the screen.
            </SheetDescription>
          </SheetHeader>
        </SheetPopup>
      </Sheet>
      <Sheet>
        <SheetTrigger
          render={(props) => (
            <Button {...props} variant="secondary">
              Open Left
            </Button>
          )}
        />
        <SheetPopup side="left" showCloseButton={false}>
          <SheetHeader>
            <SheetTitle>Left</SheetTitle>
            <SheetDescription>
              Left side of the screen.
            </SheetDescription>
          </SheetHeader>
        </SheetPopup>
      </Sheet>
      <Sheet>
        <SheetTrigger
          render={(props) => (
            <Button {...props} variant="secondary">
              Open Top
            </Button>
          )}
        />
        <SheetPopup side="top" showCloseButton={false}>
          <SheetHeader>
            <SheetTitle>Top</SheetTitle>
            <SheetDescription>Top of the screen.</SheetDescription>
          </SheetHeader>
        </SheetPopup>
      </Sheet>
      <Sheet>
        <SheetTrigger
          render={(props) => (
            <Button {...props} variant="secondary">
              Open Bottom
            </Button>
          )}
        />
        <SheetPopup side="bottom" showCloseButton={false}>
          <SheetHeader>
            <SheetTitle>Bottom</SheetTitle>
            <SheetDescription>Bottom of the screen.</SheetDescription>
          </SheetHeader>
        </SheetPopup>
      </Sheet>
    </div>
  )
}

```

## API reference [#api-reference]

All Sheet parts forward their matching Base UI Dialog props. `SheetOverlay` aliases `SheetBackdrop` and `SheetContent` aliases `SheetPopup`.

| Part               | Renders   | Notes                                                                 |
| ------------------ | --------- | --------------------------------------------------------------------- |
| `Sheet`            | Root      | Controlled (`open` / `onOpenChange`) or uncontrolled open state       |
| `SheetTrigger`     | Button    | Opens the panel; accepts Base UI `render` composition                 |
| `SheetPortal`      | Portal    | Mounts the panel outside the layout tree                              |
| `SheetBackdrop`    | Div       | Dimmed, blurred overlay; click dismisses                              |
| `SheetPopup`       | Div       | The panel itself                                                      |
| `SheetHeader`      | Div       | Title and description block                                           |
| `SheetFooter`      | Div       | Pinned to the panel bottom; buttons stack full-width on small screens |
| `SheetTitle`       | Heading   | Names the panel for assistive technology                              |
| `SheetDescription` | Paragraph | States context or consequence                                         |
| `SheetClose`       | Button    | Explicit dismissal control                                            |

`SheetPopup` adds:

| Prop              | Values                           | Default |
| ----------------- | -------------------------------- | ------- |
| `side`            | `top`, `right`, `bottom`, `left` | `right` |
| `showCloseButton` | boolean                          | `true`  |

It also accepts `initialFocus` and `finalFocus` to direct focus when the panel opens and closes. Treat the sheet as a modal dialog: provide a title, restore focus on close, and keep essential actions reachable at zoom. See the [Base UI Dialog API](https://base-ui.com/react/components/dialog#api-reference).
