Skip to content

App Model

A NativeDesktop app is a React tree rooted at a <window> intrinsic, rendered with render() from @nativedesktop/react:

import { render, useState } from "@nativedesktop/react";
function App(): React.ReactNode {
const [clicks, setClicks] = useState(0);
return (
<window title="My App" defaultWidth={480} defaultHeight={320}>
<box orientation="vertical" spacing={8}>
<label text={`Clicks: ${clicks}`} />
<button label="Increment" onClick={() => setClicks((c) => c + 1)} />
</box>
</window>
);
}
await render(<App />);

An app can mount more than one <window> at once. Render several <window> roots, for example in a fragment, and each becomes an independent OS window. See Multi-Window for the details, including how to move a live widget between windows without reloading it.

Native chrome (headerbars, toolbars, split views) is composed the same way as any other widget: declared as JSX children rather than configured through an imperative window API. examples/notes/main.tsx builds a two-pane app entirely out of intrinsics:

<window title="ND Notes" defaultWidth={900} defaultHeight={600}>
<splitview sidebarWidth={0.28}>
<toolbarview slot="sidebar">
<headerbar>
<button iconName="document-new" onClick={createNote} slot="start" />
</headerbar>
{/* sidebar content */}
</toolbarview>
<toolbarview slot="content">
<headerbar title={selected?.title ?? "ND Notes"} />
{/* content pane */}
</toolbarview>
</splitview>
</window>

slot here is an attached prop, set on a child to tell its container widget where that child belongs: sidebar or content for <splitview>, start or end for <headerbar>. Attached props apply at attach time only, so changing one after mount is a no-op. See Windows & Chrome for how these compose on each platform today.

Widget events arrive as ordinary React props (onClick, onChanged, onToggled, onSelectionChanged, and so on), each wired from the schema’s events list for that widget. <textinput onChanged={(e) => setText(e.text)} /> receives the new text on its event payload, exactly like any other controlled-component callback.

Not every prop can be changed after a widget mounts. The schema marks each prop’s appliesTo as create (set once, at construction), createAndUpdate (can change on any re-render), or meta (framework bookkeeping, e.g. testID). Button.label, for example, is create-only: changing it on a live button is a no-op, which is why examples/notes/main.tsx keys its header on the note’s id/title to force a remount instead of relying on a prop update. Check a widget’s Applied column before assuming a prop is live-updatable; the full breakdown is in the Widget Reference.