Skip to content

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.

A showAlert confirmation sheet on macOS (AppKit)

A showAlert AdwAlertDialog on GNOME (GTK)

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.

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 time

showAbout 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.