Skip to content
UI

Sheet

A panel that slides in from an edge of the screen for longer forms and record details.

@zenoxai/ui/components/sheetBase UI Markdown
<Sheet>
  <SheetTrigger render={<Button variant="outline" />}>
    Edit asset
  </SheetTrigger>
  <SheetContent>
    <SheetHeader>
      <SheetTitle>Edit asset</SheetTitle>
      <SheetDescription>
        Changes apply to new alerts. Existing alerts keep the old values.
      </SheetDescription>
    </SheetHeader>
    <FieldGroup className="px-4">
      <Field>
        <FieldLabel htmlFor="sheet-asset-name">Hostname</FieldLabel>
        <Input id="sheet-asset-name" defaultValue="srv-db-01.prod" />
      </Field>
      <Field>
        <FieldLabel htmlFor="sheet-asset-ip">IP address</FieldLabel>
        <Input id="sheet-asset-ip" defaultValue="10.20.4.17" />
      </Field>
      <Field>
        <FieldLabel htmlFor="sheet-asset-notes">Notes</FieldLabel>
        <Textarea
          id="sheet-asset-notes"
          defaultValue="Primary database for the billing tenant."
        />
      </Field>
    </FieldGroup>
    <SheetFooter>
      <SheetClose render={<Button variant="outline" />}>Cancel</SheetClose>
      <Button>Save changes</Button>
    </SheetFooter>
  </SheetContent>
</Sheet>

Usage

import { Button } from "@zenoxai/ui/components/button"
import {
  Sheet,
  SheetClose,
  SheetContent,
  SheetDescription,
  SheetFooter,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@zenoxai/ui/components/sheet"
<Sheet>
  <SheetTrigger render={<Button variant="outline" />}>Edit asset</SheetTrigger>
  <SheetContent>
    <SheetHeader>
      <SheetTitle>Edit asset</SheetTitle>
      <SheetDescription>Changes apply to new alerts.</SheetDescription>
    </SheetHeader>
    <div className="px-4">{/* body */}</div>
    <SheetFooter>
      <SheetClose render={<Button variant="outline" />}>Cancel</SheetClose>
      <Button>Save changes</Button>
    </SheetFooter>
  </SheetContent>
</Sheet>

A sheet is a Dialog docked to an edge of the screen. It behaves like one (modal, focus trapped, Escape closes it) but has the full height of the viewport, so it fits content a centered dialog cannot: a form with many fields, the details of a row picked from a table, a filter panel.

  • For a short task with one or two fields, use Dialog.
  • For a panel the user drags or swipes away on a touch screen, use Drawer. A sheet has no gestures.
  • To confirm a destructive action, use Alert Dialog.

Lay out the body as three bands: SheetHeader, the body, SheetFooter. SheetContent spaces them with a fixed gap. Header and footer carry their own padding; give the body side padding only (px-4). The footer sticks to the bottom edge.

Examples

Side

side is right by default. Use right for details and forms, left for navigation on small screens, top and bottom for short, full-width content such as a notice or a filter bar.

<>
  {(["top", "right", "bottom", "left"] as const).map((side) => (
    <Sheet key={side}>
      <SheetTrigger render={<Button variant="outline" />}>
        {side}
      </SheetTrigger>
      <SheetContent side={side}>
        <SheetHeader>
          <SheetTitle>Sheet from the {side}</SheetTitle>
          <SheetDescription>
            Top and bottom sheets span the full width. Left and right sheets
            take three quarters of a small screen and their size from the sm
            breakpoint up.
          </SheetDescription>
        </SheetHeader>
        <SheetFooter>
          <SheetClose render={<Button variant="outline" />}>
            Close
          </SheetClose>
        </SheetFooter>
      </SheetContent>
    </Sheet>
  ))}
</>

Sizes

size caps the width of left and right sheets from the sm breakpoint up: sm (25rem, the default), md (35rem), lg (50rem) and xl (68.75rem). It has no effect on top and bottom sheets. A plain className such as sm:max-w-3xl overrides it.

<>
  {(["sm", "md", "lg", "xl"] as const).map((size) => (
    <Sheet key={size}>
      <SheetTrigger render={<Button variant="outline" />}>
        Open {size}
      </SheetTrigger>
      <SheetContent size={size}>
        <SheetHeader>
          <SheetTitle>Sheet size {size}</SheetTitle>
          <SheetDescription>
            The size sets the maximum width of left and right sheets.
          </SheetDescription>
        </SheetHeader>
      </SheetContent>
    </Sheet>
  ))}
</>

Scrollable body

Make the body flex-1 overflow-y-auto so it scrolls between a fixed header and footer.

<Sheet>
  <SheetTrigger render={<Button variant="outline" />}>
    Alert history
  </SheetTrigger>
  <SheetContent size="md">
    <SheetHeader>
      <SheetTitle>srv-db-01.prod</SheetTitle>
      <SheetDescription>
        Alerts raised in the last 30 days.
      </SheetDescription>
    </SheetHeader>
    <ul className="flex-1 overflow-y-auto px-4">
      {Array.from({ length: 30 }, (_, index) => (
        <li
          key={index}
          className="flex items-center justify-between gap-4 border-b border-border py-2.5 last:border-0"
        >
          <span>Brute-force login attempt</span>
          <Badge
            variant="soft"
            tone={index % 3 === 0 ? "danger" : "warning"}
          >
            {index % 3 === 0 ? "High" : "Medium"}
          </Badge>
        </li>
      ))}
    </ul>
    <SheetFooter>
      <SheetClose render={<Button variant="outline" />}>Close</SheetClose>
      <Button>Open in alerts</Button>
    </SheetFooter>
  </SheetContent>
</Sheet>

Accessibility

  • Focus moves into the sheet when it opens and returns to the trigger when it closes. Escape and a click on the backdrop close it.
  • Always render a SheetTitle: it is the sheet’s accessible name. Hide it with className="sr-only" if the design has no visible title.
  • showOverlay={false} removes the dimmed backdrop but not the modal behavior: the page behind stays inert.

API reference

Generated from the TypeScript source. className, style and children are accepted everywhere and omitted below.

Sheet

Sheet is a panel that slides in from an edge of the screen over the page, for content that keeps the list behind it in context: a record's details, a long form, a set of filters. For a short task use Dialog; on touch screens, Drawer, which can be swiped closed.

PropTypeDefault
actionsRefRefObject<DialogRootActions | null>

A ref to imperative actions. - `unmount`: Ends the closing phase of the dialog after an externally controlled closing animation finishes. Call `preventUnmountOnClose()` in `onOpenChange` first, otherwise the dialog completes closing on its own. Whether it leaves the DOM is decided by `keepMounted` on the portal. - `close`: Closes the dialog imperatively when called.

-
defaultOpenboolean

Whether the dialog is initially open.

false
defaultTriggerIdstring | null

ID of the trigger that the dialog is associated with. This is useful in conjunction with the `defaultOpen` prop to create an initially open dialog.

-
disablePointerDismissalboolean

Whether to prevent the dialog from closing on outside presses. For non-modal dialogs, this also prevents the dialog from closing when focus moves outside of it.

false
handleDialogHandle<unknown>

A handle to associate the dialog with a trigger. If specified, allows external triggers to control the dialog's open state. Can be created with the Dialog.createHandle() method.

-
modalboolean | "trap-focus"

Determines if the dialog enters a modal state when open. - `true`: user interaction is limited to just the dialog: focus is trapped, document page scroll is locked, and pointer interactions on outside elements are disabled. - `false`: user interaction with the rest of the document is allowed. - `'trap-focus'`: focus is trapped inside the dialog, but document page scroll is not locked and pointer interactions outside of it remain enabled.

true
onOpenChange((open: boolean, eventDetails: DialogRootChangeEventDetails) => void)

Event handler called when the dialog is opened or closed.

-
onOpenChangeComplete((open: boolean) => void)

Event handler called after any animations complete when the dialog is opened or closed.

-
openboolean

Whether the dialog is currently open.

-
triggerIdstring | null

ID of the trigger that the dialog is associated with. This is useful in conjunction with the `open` prop to create a controlled dialog. There's no need to specify this prop when the dialog is uncontrolled (that is, when the `open` prop is not set).

-

SheetTrigger

PropTypeDefault
handleDialogHandle<unknown>

A handle to associate the trigger with a dialog. Can be created with the Dialog.createHandle() method.

-
idstring

ID of the trigger. In addition to being forwarded to the rendered element, it is also used to specify the active trigger for the dialog in controlled mode (with the DialogRoot `triggerId` prop).

-
nativeButtonboolean

Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`).

true
payloadunknown

A payload to pass to the dialog when it is opened.

-
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DialogTriggerState>

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

-

SheetClose

PropTypeDefault
nativeButtonboolean

Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`).

true
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DialogCloseState>

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

-

SheetContent

SheetContent is the sliding panel, in a portal over a dimmed backdrop, with a close button at its top-right corner. SheetHeader, the body and SheetFooter stack inside it 24px apart; give the body its own side padding (px-4) and no vertical padding.

PropTypeDefault
finalFocusboolean | RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null)

Determines the element to focus when the dialog is closed.

-
initialFocusboolean | RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | void | HTMLElement | null)

Determines the element to focus when the dialog is opened. By default, focus moves to the first tabbable element inside the popup, except when the dialog is opened by touch — then the popup itself is focused to avoid opening the virtual keyboard.

-
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DialogPopupState>

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

-
showCloseButtonboolean

Renders the X button at the top-right corner. Its accessible name is the English "Close": to translate it, turn this off and render your own SheetClose.

true
showOverlayboolean

Renders the dimmed backdrop. Turning it off does not make the sheet non-modal; set `modal={false}` on Sheet for that.

true
side"left" | "right" | "top" | "bottom"

Edge the sheet slides in from. Defaults to right.

right
size"sm" | "lg" | "md" | "xl"

Maximum width of a left or right sheet from the sm breakpoint: `sm` 400px (default), `md` 560px, `lg` 800px, `xl` 1100px. Below it the sheet is 75% of the viewport wide; top and bottom sheets always span the full width.

sm

SheetHeader

SheetHeader is the top band of the sheet, for its title and description.

No props of its own. Accepts the props of the element it renders.

SheetFooter

SheetFooter is the action bar on a muted band, pushed to the bottom of the sheet.

No props of its own. Accepts the props of the element it renders.

SheetTitle

PropTypeDefault
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DialogTitleState>

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

-

SheetDescription

PropTypeDefault
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DialogDescriptionState>

Allows you to replace the component's HTML element with a different tag, or compose it with another component.

-