elements
Demos UI Feed Discuss Install
Style System
  • Style
  • Themes
  • Behaviors
Forms
  • Button
  • ButtonGroup
  • Checkbox
  • ColorPicker
  • Combobox
  • Field
  • FileUpload
  • Input
  • InputGroup
  • InputOTP
  • Label
  • NativeSelect
  • NumberInput
  • PasswordInput
  • RadioGroup
  • Select
  • Slider
  • Switch
  • TagsInput
  • Textarea
  • Toggle
  • ToggleGroup
Overlays
  • AlertDialog
  • Command
  • ContextMenu
  • Dialog
  • Drawer
  • DropdownMenu
  • HoverCard
  • Menubar
  • NavigationMenu
  • Popover
  • Sheet
  • Toast
  • Tooltip
Display
  • Accordion
  • Alert
  • AspectRatio
  • Avatar
  • Badge
  • Breadcrumb
  • Card
  • Collapsible
  • Empty
  • Item
  • Kbd
  • Pagination
  • Progress
  • Separator
  • Skeleton
  • Spinner
  • Table
  • Tabs
  • Tree
Layout
  • Carousel
  • Resizable
  • ScrollArea
  • Sidebar
Dates
  • Calendar
  • DatePicker
  • DateRangePicker
  • TimePicker
Data
  • AreaChart
  • BarChart
  • Chart
  • DataTable
  • Kanban
  • LineChart
  • PieChart
  • RadarChart
  • RadialChart
  • VirtualList
Chat
  • Attachment
  • Message
  • MessageList
  • PromptInput
Elements UI / Overlays / Sheet

Sheet

A dialog that slides in from an edge of the screen, for content that complements the page.

Basic

Slides in from the right. Escape, a click outside, the X or Close dismiss it.

reasons: details:
Code
<Sheet title="Edit profile" description="Make changes to your profile here. Click save when you're done.">
  <Button slot="trigger">Open</Button>
  <SheetBody>
    <div class="field">
      <label for="name">Name</label>
      <input id="name" value={form.name}>
    </div>
  </SheetBody>
  <SheetFooter>
    <Button variant="primary" onclick={() => save(form)}>Save changes</Button>
    <SheetClose><Button>Close</Button></SheetClose>
  </SheetFooter>
</Sheet>

Usage

import { Sheet, SheetBody, SheetFooter, SheetClose } from "@elements/ui/sheet";

<Sheet side="right" title="Edit profile" description="Make changes, then save.">
  <Button slot="trigger">Edit profile</Button>
  <SheetBody>
    <input value={form.name}>
  </SheetBody>
  <SheetFooter>
    <Button variant="primary" onclick={() => save(form)}>Save changes</Button>
    <SheetClose><Button>Close</Button></SheetClose>
  </SheetFooter>
</Sheet>

<!-- controlled: open is two-way -->
<Sheet open={state.filters} side="left" title="Filters">...</Sheet>

Examples

Side

side picks the edge: top, right, bottom or left.

Code
<Sheet side="top" title="Edit profile">...</Sheet>
<Sheet side="right" title="Edit profile">...</Sheet>
<Sheet side="bottom" title="Edit profile">...</Sheet>
<Sheet side="left" title="Edit profile">...</Sheet>

No close button

closeButton={false} hides the X; the footer closes it.

Code
<Sheet title="Choose a plan" closeButton={false}>
  <Button slot="trigger">Open sheet</Button>
  ...
  <SheetFooter>
    <SheetClose><Button variant="primary">Continue</Button></SheetClose>
  </SheetFooter>
</Sheet>

Scrolling content

SheetBody scrolls when the content is taller than the sheet; the header and footer stay put. With scroll set to outside the sheet grows with its content and scrolls as one instead. initialFocus puts focus on a static paragraph at the top (tabindex=-1), so the start stays in view rather than jumping to the Accept button.

Code
<Sheet title="Terms of service" initialFocus="#terms-start">
  <Button slot="trigger">Read terms</Button>
  <SheetBody>
    <p id="terms-start" tabindex="-1">The first section takes focus, so it stays in view.</p>
    ...long text scrolls here...
  </SheetBody>
  <SheetFooter>...stays in place...</SheetFooter>
</Sheet>

<!-- scroll="outside": the sheet grows with its content and scrolls as one -->
<Sheet side="bottom" scroll="outside" title="Release notes">...</Sheet>

Controlled

Bind open to state. The sheet writes false back when it closes itself; onOpenChange reports its own requests; onOpenChangeComplete fires when the animation ends.

open: false changes: complete:
Code
<Button onclick={() => state.open = true}>Open from anywhere</Button>
<Sheet open={state.open} side="left" title="Navigation"
       onOpenChange={(open) => log(open)}
       onOpenChangeComplete={(open) => done(open)}>...</Sheet>

Sizes

size sets the side sheet's max width: sm, md (default), lg, xl or full.

Code
<Sheet size="lg" title="Order #1042">...</Sheet>
<Sheet size="sm" title="Quick note">...</Sheet>
<Sheet size="xl" title="Compare plans">...</Sheet>
<Sheet size="full" title="Preview">...</Sheet>

Non-modal

modal={false} with dismissOnOutside={false}: no backdrop and no focus trap; the page stays usable while the sheet is open.

Code
<Sheet title="Inspector" modal={false} dismissOnOutside={false}>
  <Button slot="trigger">Open inspector</Button>
  ...
</Sheet>

Header parts

SheetHeader, SheetTitle and SheetDescription take rich content and are wired to aria-labelledby and aria-describedby.

Code
<Sheet side="left">
  <Button slot="trigger">Menu</Button>
  <SheetHeader>
    <SheetTitle>Acme <em>Cloud</em></SheetTitle>
    <SheetDescription>Signed in as ada@acme.dev</SheetDescription>
  </SheetHeader>
  <nav>...</nav>
</Sheet>

Visually hidden title

A SheetTitle with class="sr-only" is invisible but still names the sheet for screen readers, through aria-labelledby.

Code
<Sheet side="right" size="sm">
  <Button slot="trigger">Notifications</Button>
  <SheetTitle class="sr-only">Notifications</SheetTitle>
  <ul>...</ul>
</Sheet>

Right to left

side="end" follows the writing direction: under dir="rtl" it docks to the left edge.

Code
<div dir="rtl">
  <Sheet side="end" title="تعديل الملف الشخصي">
    <Button slot="trigger">فتح</Button>
    ...
  </Sheet>
</div>

Nested

A sheet opened from a sheet. Escape closes the top one. The outer panel gets data-nested-dialog-open and --nested-dialogs, the inner one data-nested, for styling the stack.

Code
<Sheet title="Settings">
  <Button slot="trigger">Open settings</Button>
  <Sheet side="right" size="sm" title="Advanced">
    <Button slot="trigger">Advanced</Button>
    ...
  </Sheet>
</Sheet>

<!-- the outer panel gets data-nested-dialog-open and --nested-dialogs;
     the inner one gets data-nested -->

Focus and dismissal

initialFocus picks what takes focus on open and returnFocus={false} leaves focus where it lands on close. onEscapeKeyDown and onPointerDownOutside run before a dismiss, and preventDefault keeps the sheet open. dismissOnEscape={false} ignores Escape.

log:
Code
<Sheet title="Invite" initialFocus="#invite-email" returnFocus={false}
       onEscapeKeyDown={(e) => log("escape")}
       onPointerDownOutside={(e) => { e.preventDefault(); log("outside"); }}>
  <Button slot="trigger">Invite</Button>
  <input id="invite-email">
  <SheetFooter><SheetClose><Button>Done</Button></SheetClose></SheetFooter>
</Sheet>

<Sheet title="Uploading" dismissOnEscape={false}>...</Sheet>

Multiple triggers

Buttons anywhere on the page open one controlled sheet and pass what it shows through page state. Focus returns to the button used. Opening it from page state is the caller's change, so onOpenChange reports only the sheet's own close.

reasons:
Code
<Button onclick={() => edit(state, "Ada Lovelace")}>Edit Ada</Button>
<Button onclick={() => edit(state, "Grace Hopper")}>Edit Grace</Button>

<Sheet open={state.editing} title={state.person}
       onOpenChange={(open, reason) => log(reason)}>
  ...
</Sheet>

function edit(state, person) {
  state.person = person;   // the payload
  state.editing = true;    // focus returns to the button used
}

Trap focus

modal="trap-focus": Tab stays inside the sheet, but the page keeps scrolling, has no backdrop and takes clicks. A press outside calls onInteractOutside and closes it.

outside: 0
Code
<Sheet modal="trap-focus" title="Filters" onInteractOutside={(e) => count()}>
  <Button slot="trigger">Filters</Button>
  ...
</Sheet>

Open from a menu

A menu item opens a controlled sheet. The item is gone by the time it closes, so focus goes back to the menu's button.

Code
<DropdownMenu>
  <Button slot="trigger">Project</Button>
  <DropdownMenuItem onSelect={() => state.settings = true}>Settings...</DropdownMenuItem>
</DropdownMenu>
<Sheet open={state.settings} title="Project settings">...</Sheet>

Focus by interaction type

initialFocus and finalFocus can be functions of how the sheet opened or closed: keyboard, mouse, touch or pen.

Code
<Sheet title="Find a file"
       initialFocus={(type) => type === "keyboard" ? "[data-search]" : true}
       finalFocus={(type) => type === "keyboard" ? true : "[data-after]"}>
  <Button slot="trigger">Find a file</Button>
  <SheetBody>
    <input aria-label="Folder" value="Documents">
    <input aria-label="Search" data-search>
  </SheetBody>
</Sheet>

Focus on close and focus events

finalFocus names where focus goes on close. onOpenAutoFocus and onCloseAutoFocus run before focus moves, and preventDefault() leaves focus to the handler.

closeFocus: 0
Code
<Sheet title="Add filter" finalFocus="[data-filter-query]">...</Sheet>
<input data-filter-query>

<Sheet title="Share"
       onOpenAutoFocus={(e) => { e.preventDefault(); copy.focus(); }}
       onCloseAutoFocus={(e) => e.preventDefault()}>
  ...
</Sheet>

No visible title

With no title, label names the sheet through aria-label.

Code
<Sheet label="Quick actions" side="bottom">
  <Button slot="trigger">Quick actions</Button>
  ...
</Sheet>

Open on load

open={true} without a binding: the sheet is open when the page loads, then owns its state.

Code
<Sheet open={true} title="Welcome back">
  <Button slot="trigger">Welcome</Button>
  ...
</Sheet>

Close after an async submit

A controlled sheet stays open while its form saves, then the page closes it.

Code
<Button onclick={() => state.renaming = true}>Rename project</Button>
<Sheet open={state.renaming} title="Rename project">
  <form onsubmit={(e) => save(state, e)}>
    <input value={state.name}>
    <Button type="submit" variant="primary" loading={state.saving}>Save</Button>
  </form>
</Sheet>

async function save(state, e) {
  e.preventDefault();
  state.saving = true;
  await rename(state.name);
  state.saving = false;
  state.renaming = false;
}

One sheet in place of another

Add closes the address sheet and opens a confirmation in its place. OK returns focus to the button that opened the first one (APG).

Code
<Button onclick={() => state.address = true}>Add delivery address</Button>

<Sheet open={state.address} title="Add delivery address">
  ...fields...
  <SheetFooter>
    <Button variant="primary" onclick={() => { state.address = false; state.added = true; }}>Add</Button>
  </SheetFooter>
</Sheet>

<!-- opened in place of the first: OK returns focus to the page button -->
<Sheet open={state.added} side="bottom" title="Address added" closeButton={false}>
  <SheetFooter>
    <SheetClose><Button variant="primary">OK</Button></SheetClose>
  </SheetFooter>
</Sheet>

Non-modal, closes on focus out

modal={false} with the default dismissOnOutside: focus or a press leaving the sheet closes it, and onOpenChange reports focus-out.

reasons:
Code
<Sheet title="Filters" modal={false} onOpenChange={(open, reason) => log(reason)}>
  <Button slot="trigger">Filters</Button>
  ...
</Sheet>
<!-- focus leaving it closes it, with reason focus-out -->

Escape reaches the page

onEscapeKeyDown can call allowPropagation(): Escape closes the sheet and still reaches the page's own key handlers.

Code
<Sheet title="Shortcuts" onEscapeKeyDown={(e) => e.allowPropagation()}>
  <Button slot="trigger">Shortcuts</Button>
  <SheetBody>
    <p>Escape closes this sheet and still reaches the page's own key handler.</p>
  </SheetBody>
</Sheet>
Reference Sheet in the manual Every part, attribute, slot, key and data attribute.
Prev PopoverNext Toast
Product
Manual Pricing Compare
Company
History Contact
Legal
Privacy Policy Acceptable Use Terms License Agreement
© Elements Dev Corporation 2026