Skip to content

Feedback and Overlays: dialog / toast / Native Dialogs ​

Tags in this category carry the overlay z-order baseline (overlayZBase) and automatically escape ancestor clipping — no need to write escapeClipping manually.

<dialog> ​

Stable · Modal mask

A modal dialog: a 40% black mask covering the window + a centered card. In-flow children are the card's content; the area outside the card belongs to the mask. While the mask exists, it swallows all clicks beneath it.

PropTypeDescription
openboolean / functionControlled visibility switch; when closed, it is neither painted nor hit-tested
onClosefunctionDispatched when the mask is clicked / Esc is pressed; usually sets open to false inside
ChildrenelementThe card's content (centered); clicking it does not accidentally close the dialog
js
const [open, setOpen] = createSignal(false);

<button onClick={() => setOpen(true)}>Open dialog</button>

<dialog open={() => open()} onClose={() => setOpen(false)}>
  <column gap={8} padding={14}>
    <text font={16}>Confirm</text>
    <text>Click the mask or press Esc to close.</text>
    <button onClick={() => setOpen(false)}>Close</button>
  </column>
</dialog>

Esc Close Priority

One Esc press closes only one layer, with the priority: menu > dropdown > dialog / drawer. So an open dropdown inside a dialog collapses first, and another press gets to the dialog.

<drawer> ​

Stable · Modal

A drawer: a panel that slides in from a window edge, reusing the <dialog> overlay machinery — the same full-window mask, the same swallowing of clicks beneath it, and the same onClose on mask click / Esc. The only difference is that the content panel hugs an edge instead of being centered.

PropTypeNotes
openbool / functionWhether it is open; when false the whole subtree draws nothing and intercepts no clicks
sidestringright (default) / left — which edge the panel hugs (and the slide-in direction)
widthnumberPanel width, default 280 (clamped to the window width if larger)
onClosefunctionDispatched on mask click / Esc; it does not change open — under controlled semantics, whether it actually closes is decided by the script writing back
js
const [open, setOpen] = createSignal(false);

<button onClick={() => setOpen(true)}>
  <text>Open drawer</text>
</button>
<drawer open={() => open()} side="right" width={280} onClose={() => setOpen(false)}>
  <column gap={10} padding={16}>
    <text>Drawer content</text>
    <button onClick={() => setOpen(false)}><text>Close</text></button>
  </column>
</drawer>

The panel slides in from its edge when opened (the slide progress is driven by the animation heartbeat — no new timers), and slides back out when open turns false.

<toast> ​

Stable · Non-modal

A lightweight notification: pinned to the top-right corner of the window, non-modal (content underneath stays clickable). It shows as soon as it's mounted to the tree; mounting/unmounting is entirely under JS control — the kernel doesn't manage timers.

PropTypeDescription
messagestringNotification text
levelstringsuccess / warn / error / info, determines the color bar
js
const [showToast, setShowToast] = createSignal(false);

const notify = () => {
  setShowToast(true);
  setTimeout(() => setShowToast(false), 3000);   // Auto-dismiss via a JS timer
};

{() => (showToast() ? <toast message="Saved successfully" level="success" /> : null)}

<tooltip> ​

Stable · Non-modal · Click-through

A hover hint: wrap a trigger element, and after the pointer rests on it for delay milliseconds, a small dark text bubble pops up beside it. Moving the mouse away, pressing a mouse button, or pressing Esc dismisses it.

PropTypeDescription
textstringHint text (single line, truncated when too long; empty string never shows)
placementstringtop / bottom / left / right, defaults to bottom; flips to the opposite side automatically when it doesn't fit
delaynumberShow delay in milliseconds, defaults to 500

<tooltip> is layout-transparent — its box is the trigger element's box. Wrap whatever you want to annotate:

js
<tooltip text="Save to the cloud (Ctrl+S)" placement="bottom">
  <button>Save</button>
</tooltip>

<tooltip text="This cannot be undone" placement="top" delay={200}>
  <button background="#c0392b">Delete</button>
</tooltip>

The tooltip never blocks interaction

The bubble is purely presentational and has no event handlers — even when it covers another control, clicks pass through to whatever is underneath. To change the font size, write font={12} on the <tooltip> itself; text inherits along the parent chain.

gx/dialog Native System Dialogs ​

Stable · Windows native only · async

Invokes the operating system's native message boxes and file pickers, not self-drawn overlays. All four APIs return a Promise.

APIReturnsDescription
alert(msg, title?)Promise<void>System message box with a single "OK"
confirm(msg, title?)Promise<boolean>OK / Cancel; returns the user's choice
openFile({title, filter})Promise<string | null>System "open file" dialog; canceling returns null, it doesn't throw
saveFile({title, filter, default})Promise<string | null>System "save file" dialog; the OS asks before overwriting; canceling returns null, it doesn't throw

filter is an array of {name, pattern}, where multiple wildcards in a pattern are separated by ;.

js
import { alert, confirm, openFile, saveFile } from "gx/dialog";

// Both event handler styles work: async function () {} and async () => {}
h("button", {
  onClick: async function () {
    const yes = await confirm("Proceed with the operation?", "Please confirm");
    log("confirm -> " + (yes ? "yes" : "no"));
  }
}, "Confirm"),

h("button", {
  onClick: async function () {
    const path = await openFile({
      title: "Pick a file",
      filter: [
        { name: "Text files", pattern: "*.txt;*.md" },
        { name: "All files", pattern: "*.*" }
      ]
    });
    log(path === null ? "cancelled" : "picked " + path);
  }
}, "Open file"),

h("button", {
  onClick: async function () {
    const out = await saveFile({ default: "report.txt" });
    log(out === null ? "cancelled" : "save to " + out);
  }
}, "Save file")
  • Only these four APIs: no custom buttons, no multi-select, no directory picking; saveFile only picks the path — write the file with fs.
  • While the dialog is modal, the UI still repaints (the system pumps messages for us), but no JS callbacks are dispatched.
  • Non-Windows backends degrade: the content is printed to stderr and the call returns immediately — confirm resolves to true, openFile / saveFile to null (treated as canceled).