Skip to content

Sheet

A sheet is a dialog. It slides up from the bottom as a tearsheet, sits in the middle as a modal, or slides in from the left as a drawer. The insides are the same in all three.

CSS
css/components/sheet.css
JS
js/components/sheet.js

01 — Variants

Tearsheet, modal and drawers

Each button opens a real sheet. The left drawer here is the site’s menu; the right one shows the details of one record.

Details drawer

.sheet--end slides in from the right, for the details of one thing: a pod, an order, a file. Its title is a heading, with a small label above it (.sheet__eyebrow) for the kind of thing or where it lives. A short form title such as “New post” can stay a label.

Pod · default

api-7f9c4d-x2k8q

HTML
<dialog class="sheet sheet--end" id="pod-sheet" data-component="sheet" aria-labelledby="pod-title">
  <div class="sheet__panel panel panel--content">
    <header class="sheet__head">
      <div class="sheet__titles">
        <p class="sheet__eyebrow label">Pod · default</p>
        <h2 class="sheet__title" id="pod-title">api-7f9c4d-x2k8q</h2>
      </div>
      <button class="btn btn--ghost btn--icon" type="button" data-sheet-close aria-label="Close">…</button>
    </header>
    <div class="sheet__body">…</div>
  </div>
</dialog>

Markup

Swap .sheet--bottom for .sheet--center, .sheet--start or .sheet--end. Any element with data-sheet-open="id" opens it; data-sheet-close closes it.

HTML
<button class="btn" type="button" data-sheet-open="edit-sheet">Edit</button>

<dialog class="sheet sheet--bottom" id="edit-sheet" data-component="sheet" aria-labelledby="edit-sheet-title">
  <div class="sheet__panel panel panel--content">
    <header class="sheet__head">
      <h2 class="sheet__title label" id="edit-sheet-title">Edit photo</h2>
      <button class="btn btn--ghost btn--icon" type="button" data-sheet-close aria-label="Close">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg>
      </button>
    </header>
    <div class="sheet__body">…</div>
  </div>
</dialog>

02 — Behavior

Script

NameDetail
FocusMoves into the sheet, stays there, and returns to the button that opened it.
ClosingEsc, the close button, or a click on the dimmed page. A form with unsaved changes asks first.
PhonesThe tearsheet goes full height and shrinks above the on-screen keyboard. The page can’t scroll behind it.
jewel:sheetopen, jewel:sheetcloseFire on the dialog.
dialog.jewelSheet.open(opener), .close({ force }), .requestClose().

03 — Usage

Use it for

  • Tearsheet: a task people finish and leave, such as a post form. Best on phones.
  • Modal: a short decision or a small form.
  • Drawer: navigation on phones.

Avoid

  • Content people need to read alongside the page.
  • A sheet that opens another sheet.
  • Warnings that block for no reason. Use a notice in the page.

04 — Accessibility

Notes

  • A native <dialog>, labeled by its title with aria-labelledby.
  • The close button is an icon button with aria-label="Close".

05 — Without Jewel’s JS

What the script does

The script opens and closes the <dialog>, asks before discarding a changed form, stops the page scrolling and returns focus. The CSS reads only the markup below, so a framework that owns open and closed can drive it and leave out sheet.js and data-component.

StateSet thisNotes
Opendialog.showModal()Not the open attribute alone: the backdrop, top layer and inert page come from the modal. In React, call it and close() from an effect when your state changes.
Closeddialog.close()CSS slides it out once [open] goes.
Page scrollhas-open-sheet on <html> while it’s openStops the page scrolling behind it.
Height on phones--sheet-vh on the dialog, in pxOptional. The script sets it to visualViewport.height so the sheet shrinks above the on-screen keyboard. Without it, 100dvh.
Discard bar<div class="sheet__confirm" role="alertdialog"> after .sheet__headOptional. The script adds one with Keep editing and Discard buttons, hidden until a changed form is closed.
OpenersYour own click handlersdata-sheet-open and data-sheet-close are only read by the script.

Events and keyboard

  • Esc fires the dialog’s cancel event. Call preventDefault() if you want to ask first; either way, update your state on the dialog’s close event.
  • A click on the backdrop lands on the dialog itself (event.target === dialog). The script also checks that the press started there, so a text selection dragged outside doesn’t close it.
  • On open, the script focuses [autofocus] or the first field in .sheet__body, except on touch screens. On close, return focus to the button that opened it.
  • While it’s open, keep the focused field in view once the on-screen keyboard is up.
  • jewel:sheetopen and jewel:sheetclose don’t fire without the script.

Tearsheet

A tearsheet slides up from the bottom. On a phone it fills the screen.

Delete this photo?

It’s removed from your library and from any albums. You can’t undo this.

Pod · default

api-7f9c4d-x2k8q

Running

Node
aks-pool-1-38291746-vmss000002
app.kubernetes.io/managed-by
Helm
Started
11 Oct 2026, 14:02
Restarts
0