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 />);

Every example today mounts exactly one <window>; multi-window trees aren’t exercised or documented yet.

Native chrome — headerbars, toolbars, split views — is composed the same way as any other widget: declared as JSX children, not 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/content for <splitview>, start/end for <headerbar>). Attached props are attach-time-only: 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 — see the full breakdown in the Widget Reference.