Desktop components

Dialogs & sheets

A dialog presents a scoped task in its own window, like saving a document. A sheet presents the same kind of task attached to the top of its parent context. Both share one surface: an opaque background, a 16px smoothed radius, and the window shadow with its 1px keyline.

When to use them

Use a dialog when the task stands alone, and a sheet when the task belongs to one view. For short confirmations, use alerts instead.

Preview

Anatomy

The window is 390px wide by default, padded 20px. Form rows use a two-column grid: a 99px column of right-aligned 13px labels in the secondary text color, an 8px gutter, then 223px controls. Rows sit on a 36px rhythm (24px controls with 12px gaps). When the dialog has a form, the 13px bold title and the 11px message (14px line height) line up with the control column, 6px apart. The form starts 20px below the message, and the footer 22px below the last row.

Tune the columns with --p-dialog-label-width, --p-dialog-field-width and --p-dialog-row-gap on the dialog.

ClassRole
.p-dialogThe window surface. Radius 16, 20px padding, window shadow plus 1px keyline.
.p-dialog__title13px bold heading.
.p-dialog__message11px supporting copy.
.p-dialog__formTwo-column grid of labels and controls.
.p-dialog__labelRight-aligned row label, secondary color.
.p-dialog__control-groupA control plus a trailing companion, like a popup and its 24px disclosure button.
.p-dialog__footerAction row, right-aligned.
.p-dialog__footer-leadingPushes one action (usually destructive) to the left edge.
.p-sheetThe same surface presented from the top of a host.
.p-sheet-hostPositioning context for an attached sheet.

Fields, selects, and buttons inside a dialog are the standard form controls and buttons. Nothing is restyled.

Expanded dialog

The expanded form is the full save panel: 1000 by 448. A 210px sidebar runs flush down the leading edge, with the traffic lights at the top, a 36px round sidebar toggle in the corner, and 24px source rows with 8px corners. The main pane stacks a centered form (37px row pitch), a toolbar row, the file browser, and the footer. The browser runs flush to the pane edges between two 1px keylines; its rows are 20px tall, zebra-striped with 8px rounded ends, and inset 10px.

ClassRole
.p-dialog--expanded1000×448 window, no padding, sidebar and main pane side by side.
.p-dialog__sidebarFlush 210px sidebar.
.p-dialog__sidebar-header52px band holding .p-traffic-lights (from windows.css) and the toggle.
.p-dialog__sidebar-toggle36px round glass button, 8px from the top and trailing edge.
.p-dialog__source-heading11px bold section title.
.p-dialog__source24px source row. Mark the current one with aria-current.
.p-dialog__source-iconAccent-tinted row icon.
.p-dialog__mainThe main pane.
.p-dialog__toolbarOptional row of controls between the form and the browser.
.p-dialog__browserScrolling file area between two 1px keylines.
.p-dialog__filesThe file table: 28px header, 20px zebra rows. .p-dialog__files-num right-aligns a column.

Place the destructive action on the leading edge with .p-dialog__footer-leading, and end with Cancel then the primary action. This mirrors the order people expect on macOS: destruction far from confirmation.

<div class="p-dialog__footer">
  <button class="p-button p-button-destructive p-dialog__footer-leading">Delete</button>
  <button class="p-button" data-p-cancel>Cancel</button>
  <button class="p-button p-button-primary">Save</button>
</div>

Opening a dialog

Dialogs run through the same modal manager as alerts. Add .p-modal next to .p-dialog, put it inside .p-modal-background, and open it with data-p-open-modal.

<button class="p-button p-button-primary" data-p-open-modal="#save-dialog">Save…</button>

<div class="p-modal-background">
  <div
    class="p-modal p-dialog"
    id="save-dialog"
    data-p-close-on-outside="true"
    role="dialog"
    aria-modal="true"
    aria-hidden="true"
    aria-labelledby="save-dialog-title"
  >
    <h2 class="p-dialog__title" id="save-dialog-title">Save this document?</h2>
    <div class="p-dialog__form">
      <label class="p-dialog__label" for="save-name">Save As:</label>
      <input class="p-field" id="save-name" type="text" value="Untitled" />
    </div>
    <div class="p-dialog__footer">
      <button class="p-button" data-p-cancel>Cancel</button>
      <button class="p-button p-button-primary">Save</button>
    </div>
  </div>
</div>

<script src="path/to/puppertino/src/js/modals.js"></script>

Sheets

A sheet is the same surface sliding down from the top of its host. Inside a .p-sheet-host it attaches to that container. Inside .p-modal-background it attaches to the viewport. The entrance uses the 0.35s sheet timing.

<div class="p-modal-background">
  <div
    class="p-modal p-sheet"
    id="rename-sheet"
    role="dialog"
    aria-modal="true"
    aria-hidden="true"
    aria-labelledby="rename-sheet-title"
  >
    <h2 class="p-dialog__title" id="rename-sheet-title">Rename “Quarterly Report”?</h2>
    <div class="p-dialog__form">
      <label class="p-dialog__label" for="rename-name">Name:</label>
      <input class="p-field" id="rename-name" type="text" value="Quarterly Report" />
    </div>
    <div class="p-dialog__footer">
      <button class="p-button" data-p-cancel>Cancel</button>
      <button class="p-button p-button-primary">Rename</button>
    </div>
  </div>
</div>

Dark mode

The surface swaps to #1E1E1E and the shadow deepens to 57%, with the same black keyline. Sheets also get a 0.5px inner stroke of white at 20% so their edge holds against a dark window. In the expanded dialog the sidebar turns #161616, zebra stripes and dividers switch to white at 5% and 10%, and row icons brighten to #0A99FF. Titles and copy follow the dialog text tokens. Controls inside adapt on their own.

Accessibility

Dialogs and sheets are modal: role="dialog" with aria-modal="true", labelled by their title. The modal manager traps focus, closes on Escape, and returns focus to the trigger. With reduced motion, the scale and slide transitions reduce to fades. The surface is opaque in both appearances, so text contrast does not depend on the backdrop. With prefers-contrast: more the keyline becomes a solid 1px edge.

Copied