Overlays
Sheet
A full-height panel at the edge of the screen. The same native <dialog> as
the modal — a different layout rather than a different
component.
Markup
<button class="inst-btn" type="button"
onclick="document.getElementById('dlg-params').showModal()">From the end edge</button>
<button class="inst-btn" type="button"
onclick="document.getElementById('dlg-nav').showModal()">From the start edge</button>
<dialog class="inst-dialog inst-sheet inst-sheet--start" id="dlg-nav">
<form method="dialog">
<div class="inst-dialog-head">
<span class="inst-dialog-title">Sections</span>
<button class="inst-btn inst-btn--sm inst-btn--ghost inst-btn--icon inst-dialog-close" type="submit" aria-label="Close">
<svg class="inst-icon" aria-hidden="true"><use href="#i-close"/></svg>
</button>
</div>
<div class="inst-dialog-body">
<nav class="inst-nav" aria-label="Sections">
<a class="inst-nav-item" href="#" aria-current="page">Runs</a>
<a class="inst-nav-item" href="#">Artefacts</a>
<a class="inst-nav-item" href="#">Settings</a>
</nav>
</div>
</form>
</dialog>
<dialog class="inst-dialog inst-sheet" id="dlg-params">
<form method="dialog">
<div class="inst-dialog-head">
<span class="inst-dialog-title">Parameters of the run</span>
<button class="inst-btn inst-btn--sm inst-btn--ghost inst-btn--icon inst-dialog-close" type="submit" aria-label="Close">
<svg class="inst-icon" aria-hidden="true"><use href="#i-close"/></svg>
</button>
</div>
<div class="inst-dialog-body">
<dl class="inst-kv">
<dt>Model</dt><dd>opus</dd>
<dt>Agents</dt><dd>7 of 12</dd>
<dt>Tokens</dt><dd>184 320</dd>
<dt>Started</dt><dd>19:38:04</dd>
</dl>
</div>
<div class="inst-dialog-foot">
<button class="inst-btn" type="submit">Close</button>
</div>
</form>
</dialog>
Contract
inst-sheet is a modifier over inst-dialog rather than a replacement: the
class of the base stays, and the parts (-head, -body, -foot) are the
same.
| What | Required | Why |
|---|---|---|
An inst-dialog together with inst-sheet |
yes | The surface, the shadow, the ground and the layout of the parts come from the base |
A native <dialog> |
yes | Modality and Escape come from the platform |
showModal() |
yes | show() opens it non-modally: no ground and no inert background |
A <form method="dialog"> inside |
yes | Closing by a button with no script |
An aria-label on the cross |
yes | Otherwise a button with no text has no accessible name |
Accessibility
| Focus | The focus trap and the return to the button that called it come from showModal() |
| The keyboard | Escape closes. Buttons inside a <form method="dialog"> close on a press |
| The background is inert | The ground switches the background off for the mouse and for a screen reader. If access to the background is wanted, this is not a sheet but a column of the shell |
| Scrolling the background | It stops by the same html:has(dialog:modal) rule as on a modal |
| Height | 100dvh rather than 100vh: on mobile the browser's panel does not cut off the footer with its buttons |
| A sheet is not printed on paper |
Anatomy
Every part comes from the modal: inst-dialog-head,
inst-dialog-title, inst-dialog-close, inst-dialog-body,
inst-dialog-foot, inst-dialog-foot--start, inst-dialog-foot-note.
The modifier changes the layout:
| Property | The modal | The sheet |
|---|---|---|
| width | min(34rem, 100vw - var(--space-8)) |
min(28rem, 100vw) |
| height | min(80dvh, 100dvh - var(--space-8)) |
100dvh |
| margins | margin: auto — centred |
Pushed to the edge |
| radius | --radius-lg |
0 — the block meets the edge of the screen |
| border | Around the perimeter | On the side of the content only |
On a narrow screen min(28rem, 100vw) gives a full-width sheet — there is no
need to start a separate mobile variant.
Variants
<!-- At the end edge (the default) -->
<dialog class="inst-dialog inst-sheet">…</dialog>
<!-- At the start edge -->
<dialog class="inst-dialog inst-sheet inst-sheet--start">…</dialog>
| Class | The edge | The boundary |
|---|---|---|
inst-sheet |
The end one (the right in LTR) | A hairline along the start edge |
inst-sheet--start |
The start one (the left in LTR) | A hairline along the end edge |
The sides are logical rather than physical: in RTL a sheet mirrors itself, and nothing has to be edited.
JS
Opening is one line. Everything else is taken on by the platform.
document.getElementById('dlg').showModal();
| What | Who does it |
|---|---|
| Opening | The application: showModal() |
| Closing | A <form method="dialog"> inside — with no script |
The ground, the top layer, Escape |
The platform |
| Returning the focus to the button that opened it | The platform |
| Blocking the scroll of the background | The library, by the rule html:has(dialog:modal) |
The declarative command and commandfor would spare even that line, but they
are not supported everywhere yet and are not taken into the library's contract.
API
| Name | What it does |
|---|---|
| class | |
inst-sheet | A full-height layout at the end edge. Only together with inst-dialog |
| modifier | |
inst-sheet--start | The same block at the start edge |
| Name | Value | Where from |
|---|---|---|
| token | ||
--surface-overlay | light-dark(var(--n-0), var(--n-11)) | base +1: theme |
--shadow-modal | 0 16px 40px -8px var(--shadow-color-far), 0 4px 10px -4px var(--shadow-color-near) | one across every cell |
--scrim | light-dark(oklch(0 0 0 / 0.32), oklch(0 0 0 / 0.58)) | one across every cell |
--border | light-dark(oklch(0 0 0 / 0.12), oklch(1 0 0 / 0.11)) | one across every cell |
--hairline | 1px | base +1: screen |
--pad-card | var(--space-6) | base +14: density · scale |
--space-5 | 12px | one across every cell |
--gap-inline | var(--space-4) | base +14: density · scale |
--text-md | 1rem | base +4: scale |
Why the kit works this way — the constitution · Open source under MIT, github.com/keshon/instrument