Dialogs
@nativedesktop/react exposes four native, per-window modal dialogs (a confirmation alert, an
open-file panel, a save-file panel, and the app’s About panel) as promise-returning functions layered
over the <window> widget’s imperative commands. They render
as NSAlert/NSOpenPanel/NSSavePanel sheets on macOS and AdwAlertDialog/GtkFileDialog on GTK.


Wiring a window for dialogs
Section titled “Wiring a window for dialogs”Because a modal dialog is one-per-window on both backends, these calls correlate their result to the
<window>’s own wire id rather than a generated per-call token. That means the window needs a ref
and three result-event props wired back to the matching helper, once, regardless of how many places
in your tree trigger a dialog:
import { showAlert, openFile, saveFile, showAbout, onAlertResult, onOpenFileResult, onSaveFileResult,} from "@nativedesktop/react";import type { NdNodeRef } from "@nativedesktop/react";
function App() { const winRef = useRef<NdNodeRef<"window">>(null); const [result, setResult] = useState("(none yet)");
async function handleDelete() { if (!winRef.current) return; const { buttonId } = await showAlert(winRef.current, { title: "Delete this item?", body: "This action cannot be undone.", buttons: [ { id: "cancel", label: "Cancel" }, { id: "delete", label: "Delete", style: "destructive" }, ], }); setResult(buttonId); }
return ( <window ref={winRef} title="My App" onAlertResult={(e) => onAlertResult(winRef.current!, e)} onOpenFileResult={(e) => onOpenFileResult(winRef.current!, e)} onSaveFileResult={(e) => onSaveFileResult(winRef.current!, e)} > <button label="Delete…" onClick={handleDelete} /> </window> );}The on*Result props aren’t optional: skip one and its matching call’s promise never settles,
because the result event is how the promise learns the dialog closed. showAbout has no result
event and needs no wiring; see below.
| Function | Options | Resolves to |
|---|---|---|
showAlert(node, options) |
{ title, body?, buttons: { id, label, style? }[] } |
{ buttonId }, the clicked button’s id |
openFile(node, options?) |
{ multiple?, directories?, filters?: { name, extensions }[] } |
{ canceled, paths }, with paths: [] if canceled |
saveFile(node, options?) |
{ suggestedName?, defaultDir?, filters? } |
{ canceled, path }, with path: null if canceled |
showAbout(node, options) |
{ appName, version, developer?, website? } |
none; fire-and-forget, no promise |
style on an alert button is "default" | "suggested" | "destructive". The destructive style is the
red warning treatment: NSAlertStyle.critical-adjacent styling on macOS, .destructive-action on
GTK.
One dialog per window at a time
Section titled “One dialog per window at a time”showAlert/openFile/saveFile each claim their window’s single dialog slot for as long as they’re
pending; neither backend has a way to stack two native sheets on one window. Calling a second one
before the first resolves rejects immediately with an error naming the dialog that is still open,
rather than queueing or silently clobbering the first caller’s promise:
Error: <window> already has a "showAlert" dialog pending; only one modal dialog per window is allowed at a timeshowAbout is the exception: it has no result event to correlate, so it doesn’t claim the slot and
can be called freely alongside a pending showAlert/openFile/saveFile.
See packages/react/src/dialogs.ts for the full implementation and examples/gallery/main.tsx’s
“Dialogs” tab for all four calls wired to readouts.