# Native SDK documentation

> Build native desktop applications with TypeScript and Native markup. Native SDK provides a native renderer, components, app state, and development tools.

## When to use Native SDK

- Category: Application framework
Common use cases:

- Build native desktop apps with TypeScript and Native markup
- Look up Native markup components and platform support
- Test and automate native apps

## Documentation

---
title: App Model
url: "https://native-sdk.dev/docs/app-model"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

A Native SDK app is one loop with four parts:

- **Model** — a plain data structure holding all app state.
- **Msg** — a tagged union of everything that can happen.
- **update(model, msg)** — the only place state changes.
- **View** — a Native markup file (`.native`, or a Zig view function) that derives the UI from the model.

The runtime manages windows, rendering, input dispatch, timers, accessibility, and hot reload. A widget dispatches its bound message to `update`; the runtime then derives the view from the committed model and renders the changes.

Both TypeScript and Zig cores use this loop. The default project has `src/core.ts`, compiled to native code at build time. A `zig-core` project has `src/main.zig`. The wiring sections below show the runtime setup that the build generates for TypeScript apps and that a Zig app can configure directly. See [TypeScript Cores](/docs/typescript) for the TypeScript contract.

## The loop in full

A minimal counter declares state, messages, and a pure `update`:

```ts title="src/core.ts"
export interface Model {
  readonly count: number;
}

export type Msg =
  | { readonly kind: "increment" }
  | { readonly kind: "decrement" }
  | { readonly kind: "reset" };

export function initialModel(): Model {
  return { count: 0 };
}

export function update(model: Model, msg: Msg): Model {
  switch (msg.kind) {
    case "increment":
      return { ...model, count: model.count + 1 };
    case "decrement":
      return { ...model, count: model.count - 1 };
    case "reset":
      return { ...model, count: 0 };
  }
}
```

```zig title="src/main.zig"
pub const Msg = union(enum) {
    increment,
    decrement,
    reset,
};

pub const Model = struct {
    count: i64 = 0,
};

pub fn update(model: *Model, msg: Msg) void {
    switch (msg) {
        .increment => model.count += 1,
        .decrement => model.count -= 1,
        .reset => model.count = 0,
    }
}
```

The view in `src/app.native` binds the model and names the messages:

```html
<row gap="8" main="center" cross="center" grow="1">
  <button variant="secondary" on-press="decrement">-</button>
  <text>{count}</text>
  <button variant="primary" on-press="increment">+</button>
</row>
```

Markup can never mutate state. `{count}` is a read; `on-press="increment"` names a `Msg` variant (in a TypeScript core, the arm's `kind`). Every state change flows through `update`, which makes the app's behavior testable as a plain function — the Zig template's generated `src/tests.zig` drives it with no GUI at all, and a TypeScript core runs under node the same way (`native dev --core`).

## Wiring

`native_sdk.UiApp(Model, Msg)` ties the loop to the runtime. A zero-config app never writes this — the build graph generates it (for a TypeScript core, over the compiled core's model) — but it is ordinary code you can own any time. From the Zig template's `main`:

```zig
const CounterApp = native_sdk.UiApp(Model, Msg);

pub fn main(init: std.process.Init) !void {
    // `create` heap-allocates the multi-MB app struct and constructs the
    // Model in place — neither ever rides the stack.
    const app_state = try CounterApp.create(std.heap.page_allocator, .{
        .name = "my_app",
        .scene = shell_scene,             // one window, one gpu_surface view
        .canvas_label = "main-canvas",    // must match the scene's view label
        .update_fx = update,              // update(model, msg, fx) - or .update for a pure app
        .markup = .{ .source = app_markup, .watch_path = "src/app.native", .io = init.io },
    });
    defer app_state.destroy();
    app_state.model = initialModel();     // boot state: assign through the pointer

    try runner.runWithOptions(app_state.app(), .{ ... }, init);
}
```

`create` requires every `Model` field to carry a default; the model starts as `.{}` and boot state is assigned through the returned pointer. The `scene` declares the native window and its GPU surface view — see [Windows](/docs/windows) and [Native Surfaces](/docs/native-surfaces) for multi-view scenes.

## Rebuilds and widget identity

After `update`, the runtime derives the view from the committed model. Reconciliation follows these rules:

- **Widget identity is structural.** A widget keeps its id across rebuilds, reorders, and hot reloads, so engine-owned state — scroll offsets, text carets, focus — survives. List items carry `key` (or `global-key` for items that move between containers) to keep identity through reorders. Unkeyed same-kind siblings take positional identity (sibling index), so an `<if>` that inserts or removes an earlier same-kind sibling re-disambiguates the trailing ones — engine-owned state like carets and scroll can hop; keyed items and keyed ancestors hold identity.
- **The source wins.** Engine-retained state (a scroll offset, a toggle) survives rebuilds until the model asserts a different value; then the model's value applies. This is why controlled patterns echo runtime-applied values back through the model — see [State & Data Flow](/docs/state).
- **Update errors are recorded.** A returned `update` error is caught and recorded in a bounded error ring (visible in [automation](/docs/automation) snapshots as `dispatch_errors=`), and the app keeps running.

## Hot reload in development

With `watch_path` set, the runtime watches the `.native` file while the app runs. Edits preserve model state and widget identity. Parse failures keep the last good view on screen and record a diagnostic (`app_state.markup_diagnostic` carries line, column, and message).

## Compile the markup for release

In release builds the markup compiles at comptime — no parser in the binary, and markup or binding mistakes become compile errors with line and column:

```zig
const dev = @import("builtin").mode == .Debug;
const App = native_sdk.UiAppWithFeatures(Model, Msg, .{ .runtime_markup = dev });
const CompiledView = canvas.CompiledMarkupView(Model, Msg, @embedFile("app.native"));
// options:
.view = CompiledView.build,
.markup = if (dev) .{ .source = app_markup, .watch_path = "src/app.native", .io = init.io } else null,
```

Both engines produce the identical widget tree — same structural ids, same typed handler table — so tests, automation scripts, and goldens hold across dev and release.

## Hybrid views: a Zig root composing markup fragments

Compiled markup views compose the other way too: a hand-written Zig builder root can build markup fragments as ordinary children. This is the pattern for any UI that mixes custom Zig panes with declarative markup — the root places what the closed grammar cannot express (a scaled `ui.paragraph` display block, a `.band`-series `ui.chart`, per-row native context menus), and the markup keeps everything it can:

```zig
const CompiledHeaderView = canvas.CompiledMarkupView(Model, Msg, @embedFile("header.native"));

pub fn rootView(ui: *Ui, model: *const Model) Ui.Node {
    return ui.column(.{ .gap = 12, .grow = 1 }, .{
        CompiledHeaderView.build(ui, model), // the markup fragment, as a child
        ui.paragraph(.{}, &.{
            .{ .text = model.readout(ui.arena), .monospace = true, .scale = 1.6 },
        }),
    });
}
// options: .view = rootView,
```

`examples/calculator` is the smallest live reference (`CompiledKeypadView.build(ui, model)` inside a hand-written root); `deck` uses the same shape. Widget ids, handlers, and dispatch are identical to a pure-markup tree, so tests and automation address the fragment's widgets the usual way.

A Zig-root app keeps dev-time hot reload for its embedded fragments too: build with `UiAppWithFeatures(Model, Msg, .{ .runtime_markup = dev_markup_reload })` and pass `.markup = .{ .source = ..., .watch_path = "src/header.native", .io = init.io }` gated on `builtin.mode == .Debug` (`null` otherwise) — the wiring in `examples/notes`. Debug builds then reload edits to the watched file in place; release builds compile the runtime engine out entirely.

## Side effects

`update` stays pure by routing anything that leaves the model — subprocesses, HTTP, file persistence, timers, clipboard, desktop notifications — through the effects channel, and routed results come back as ordinary messages. In a TypeScript core, effects are `Cmd` data returned from `update` and recurring timers are declared `Sub` data — see [TypeScript Cores: Effects](/docs/typescript#effects-are-cmd-data). In a Zig core, declare `.update_fx` instead of `.update` and spawn from message arms; boot-time work goes in `.init_fx`, which runs exactly once before the first paint. See [Native UI: Effects](/docs/native-ui#effects).

## Dropping down

`UiApp` is a layer over the lower-level `App`/`Runtime` pair, which any app can use directly — for custom lifecycle callbacks, imperative window and view management, or embedding [web content](/docs/frontend). The [App & Runtime](/docs/runtime) reference documents that layer, and [Embedded App](/docs/embed) covers driving the runtime from an existing host (including iOS and Android).

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Config
url: "https://native-sdk.dev/docs/app-zon"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The `app.json` manifest declares app metadata, permissions, security rules, window layout, and packaging inputs. It is read by the CLI and tooling at build, package, and validation time. New projects use JSON so TypeScript developers get familiar syntax, completion, and inline validation through [`$schema`](https://schema.native-sdk.dev/app/v1.json). Existing `app.zon` manifests remain fully supported and expose the same fields—there is no reduced JSON feature set. The versioned URL stays compatible for the lifetime of the v1 manifest contract; `/app.json` is the current-version alias.

## Example: native-rendered app

The manifest `native init` generates — identity, one shell window with a GPU surface view, and the minimal permission set:

```json title="app.json"
{
  "$schema": "https://schema.native-sdk.dev/app/v1.json",
  "id": "dev.native_sdk.my-app",
  "name": "my-app",
  "display_name": "My App",
  "description": "A counter that lives in one native window.",
  "version": "0.1.0",
  "icons": ["assets/icon.png"],
  "platforms": ["macos"],
  "permissions": ["view", "command"],
  "capabilities": ["native_views", "gpu_surfaces"],
  "shell": {
    "windows": [{
      "label": "main",
      "title": "My App",
      "width": 480,
      "height": 320,
      "views": [{
        "label": "main-canvas",
        "kind": "gpu_surface",
        "fill": true,
        "role": "Counter canvas",
        "accessibility_label": "Counter",
        "gpu_backend": "metal",
        "gpu_pixel_format": "bgra8_unorm",
        "gpu_present_mode": "timer",
        "gpu_alpha_mode": "opaque",
        "gpu_color_space": "srgb",
        "gpu_vsync": true
      }]
    }]
  },
  "security": {
    "navigation": {
      "allowed_origins": ["zero://app", "zero://inline"],
      "external_links": { "action": "deny" }
    }
  },
  "web_engine": "system",
  "cef": { "dir": "third_party/cef/macos", "auto_install": false }
}
```

## Example: app with web content, menus, and shortcuts

A fuller manifest for an app that also [embeds web content](/docs/frontend) and declares commands, shortcuts, menus, and packaging metadata:

```zig title="app.zon"
.{
    .id = "dev.native_sdk",
    .name = "native-sdk",
    .display_name = "native-sdk",
    .version = "0.1.0",
    .icons = .{"assets/icon.png"},
    .platforms = .{ "macos" },
    .permissions = .{ "command", "view", "dialog", "window", "clipboard", "credentials" },
    .capabilities = .{ "webview", "js_bridge", "native_views", "gpu_surfaces", "menus", "shortcuts", "dialog", "clipboard", "credentials" },
    .bridge = .{
        .commands = .{
            .{ .name = "native.ping", .origins = .{ "zero://app" } },
            .{ .name = "native-sdk.window.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
            .{ .name = "native-sdk.command.invoke", .permissions = .{ "command" }, .origins = .{ "zero://app" } },
            .{ .name = "native-sdk.view.list", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
            .{ .name = "native-sdk.dialog.showMessage", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } },
            .{ .name = "native-sdk.clipboard.readText", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
            .{ .name = "native-sdk.credentials.get", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } },
        },
    },
    .security = .{
        .navigation = .{
            .allowed_origins = .{ "zero://app", "http://127.0.0.1:5173" },
            .external_links = .{ .action = "deny" },
        },
    },
    .web_engine = "system",
    .cef = .{ .dir = "third_party/cef/macos", .auto_install = false },
    .windows = .{
        .{ .label = "main", .title = "native-sdk", .width = 720, .height = 480, .restore_state = true },
    },
    .commands = .{
        .{ .id = "command.palette", .title = "Command Palette" },
        .{ .id = "app.reload", .title = "Reload" },
    },
    .shortcuts = .{
        .{ .id = "command.palette", .key = "p", .modifiers = .{ "primary", "shift" } },
    },
    .menus = .{
        .{
            .title = "View",
            .items = .{
                .{ .label = "Command Palette", .command = "command.palette", .key = "p", .modifiers = .{ "primary", "shift" } },
                .{ .separator = true },
                .{ .label = "Reload", .command = "app.reload", .key = "r", .modifiers = .{ "primary" } },
            },
        },
    },
    .file_associations = .{
        .{
            .name = "Markdown Document",
            .extensions = .{ "md", ".markdown" },
            .mime_types = .{ "text/markdown" },
            .icon = "assets/markdown.icns",
        },
    },
    .url_schemes = .{
        .{ .scheme = "native-sdk" },
    },
}
```

## Fields

<table>
  <thead>
    <tr>
      <th>
        Field
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `id`
      </td>

      <td>
        Reverse-DNS bundle identifier (e.g.

        `com.example.myapp`

        )
      </td>
    </tr>

    <tr>
      <td>
        `name`
      </td>

      <td>
        Short machine name
      </td>
    </tr>

    <tr>
      <td>
        `display_name`
      </td>

      <td>
        Human-readable app name — the application menu, Dock, app switcher, and About panel all use it, in dev runs and packaged bundles alike
      </td>
    </tr>

    <tr>
      <td>
        `description`
      </td>

      <td>
        Optional one-line description shown in the About panel (max 256 bytes, single line)
      </td>
    </tr>

    <tr>
      <td>
        `version`
      </td>

      <td>
        Canonical

        `X.Y.Z`

        version with no leading zeroes — also the version the About panel shows
      </td>
    </tr>

    <tr>
      <td>
        `icons`
      </td>

      <td>
        The app icon: one square

        `.png`

        (1:1, ideally 1024x1024) or

        `.svg`

        source that packaging turns into every platform's icon artifacts — the macOS

        `.icns`

        gets the platform's rounded-rect icon shape applied automatically. A prebuilt

        `.icns`

        /

        `.ico`

        entry ships untouched for its platform instead
      </td>
    </tr>

    <tr>
      <td>
        `platforms`
      </td>

      <td>
        Target platforms:

        `macos`

        ,

        `linux`

        ,

        `windows`

        ,

        `ios`

        ,

        `android`

        ,

        `web`
      </td>
    </tr>

    <tr>
      <td>
        `dmg`
      </td>

      <td>
        Optional macOS DMG presentation: volume name, PNG/JPEG/TIFF background (with automatic adjacent

        `@2x`

        discovery), usable Finder canvas and icon sizes, simple app/Applications positions, or an explicit positioned

        `items`

        list of the app, Applications alias, project files/directories, and absolute links. The zero-config defaults produce a complete drag-to-Applications layout.
      </td>
    </tr>

    <tr>
      <td>
        `permissions`
      </td>

      <td>
        Runtime permissions (see

        <a href="/docs/security">Security</a>

        ). Audio capture uses

        `microphone`

        and

        `system_audio`

        ; macOS packaging emits the matching microphone, audio-capture, and screen-capture usage descriptions only when declared
      </td>
    </tr>

    <tr>
      <td>
        `capabilities`
      </td>

      <td>
        Feature declarations (see

        <a href="/docs/security">Security</a>

        ).

        `"store"`

        links the engine-owned record store;

        `"sqlite"`

        links relational SQL effects. They share one capability-shed SQLite object but use separate databases; see

        <a href="/docs/record-store">Record Store</a>

        and

        <a href="/docs/sqlite">Relational SQLite</a>
      </td>
    </tr>

    <tr>
      <td>
        `dock_visible`
      </td>

      <td>
        Initial macOS Dock and app-switcher presence. Defaults to

        `true`

        . Set

        `false`

        for an Accessory/menu-bar app; the policy is applied before startup windows are created, so no Dock tile flashes. Accessory apps must declare the

        `"tray"`

        capability. Runtime

        `Cmd.setDockPresence`

        can still promote or demote the app later.
      </td>
    </tr>

    <tr>
      <td>
        `persist`
      </td>

      <td>
        Engine-owned Model snapshot config: monotonic

        `version`

        , optional

        `debounce_ms`

        , and the

        `restore`

        Msg routes (

        `ok`

        /

        `none`

        /

        `err`

        ). Requires

        `"persist"`

        in capabilities — see

        <a href="/docs/persistence">Model Persistence</a>
      </td>
    </tr>

    <tr>
      <td>
        `images`
      </td>

      <td>
        Optional registered-image budget:

        `.images = .{ .max_image_pixel_bytes = 8_388_608 }`

        . The default is 1 MiB and accepted values are 1–8 MiB. Encoded photos decode aspect-preservingly to fit; storage is lazy per used slot, but 16 fully used 8 MiB slots are a declared 128 MiB high-water. See

        <a href="/docs/dynamic-images">Dynamic Images</a>

        .
      </td>
    </tr>

    <tr>
      <td>
        `bridge`
      </td>

      <td>
        Bridge command policies (see

        <a href="/docs/bridge">Bridge</a>

        )
      </td>
    </tr>

    <tr>
      <td>
        `security`
      </td>

      <td>
        Navigation and external link policies (see

        <a href="/docs/security">Security</a>

        )
      </td>
    </tr>

    <tr>
      <td>
        `web_engine`
      </td>

      <td>
        `system`

        or

        `chromium`

        ; Chromium is currently supported for macOS builds (see

        <a href="/docs/web-engines">Web Engines</a>

        )
      </td>
    </tr>

    <tr>
      <td>
        `webview_layer`
      </td>

      <td>
        `auto`

        (default),

        `include`

        , or

        `exclude`

        — whether the build ships the embedded web layer (see below)
      </td>
    </tr>

    <tr>
      <td>
        `theme`
      </td>

      <td>
        Built-in theme pack:

        `house`

        (default) or

        `geist`

        ; an unknown name is a build/check error (see

        <a href="/docs/theming">Theming</a>

        )
      </td>
    </tr>

    <tr>
      <td>
        `theme_accent`
      </td>

      <td>
        Optional

        `#rrggbb`

        accent layered over the pack — recolors the accent, its knockout ink, the focus ring, and the slider active range together; skipped under high contrast; a malformed value is a build/check error
      </td>
    </tr>

    <tr>
      <td>
        `cef`
      </td>

      <td>
        CEF runtime config for Chromium apps:

        `dir`

        and

        `auto_install`
      </td>
    </tr>

    <tr>
      <td>
        `assets`
      </td>

      <td>
        Launch-registered assets for TypeScript-core apps:

        `images`

        entries (

        `.{ .id = 1, .path = "assets/art/cover.jpg" }`

        ) are read once at launch and registered on the installing frame; the

        `id`

        is the

        `ImageId`

        markup avatar bindings reference
      </td>
    </tr>

    <tr>
      <td>
        `windows`
      </td>

      <td>
        Window definitions (see

        <a href="/docs/windows">Windows</a>

        )
      </td>
    </tr>

    <tr>
      <td>
        `shell`
      </td>

      <td>
        Native-first window and view tree declarations
      </td>
    </tr>

    <tr>
      <td>
        `commands`
      </td>

      <td>
        Shared command metadata for menus, shortcuts, native controls, tray items, and bridge calls
      </td>
    </tr>

    <tr>
      <td>
        `shortcuts`
      </td>

      <td>
        Keyboard shortcuts delivered as

        `shortcut`

        events (see

        <a href="/docs/keyboard-shortcuts">Keyboard Shortcuts</a>

        )
      </td>
    </tr>

    <tr>
      <td>
        `menus`
      </td>

      <td>
        Native menu declarations delivered through the command event path (see

        <a href="/docs/menus">Menus</a>

        )
      </td>
    </tr>

    <tr>
      <td>
        `file_associations`
      </td>

      <td>
        File type registration metadata for packaging
      </td>
    </tr>

    <tr>
      <td>
        `url_schemes`
      </td>

      <td>
        Custom URL scheme registration metadata for packaging
      </td>
    </tr>

    <tr>
      <td>
        `frontend`
      </td>

      <td>
        Frontend build/dev config (see

        <a href="/docs/frontend">Frontend Projects</a>

        )
      </td>
    </tr>
  </tbody>
</table>

## `webview_layer`

Under the default `auto`, the build infers whether to ship the embedded web layer from what the app declares: `"webview"` in `.capabilities`, a `.frontend` block, a `.shell` webview view, or a web engine resolved to Chromium (from `.web_engine` or the `-Dweb-engine`/`--web-engine` flags) each count as web intent, and an app with none of them builds native-only. `include` forces the layer into a build that declares nothing yet; `exclude` promises a native-only app. The `-Dweb-layer` build option overrides the manifest setting for one build.

A native-only build carries the platform consequences with it: on Windows the executable never references `WebView2Loader.dll` and no loader ships in the package; on Linux the host compiles and links without WebKitGTK entirely, so building needs no WebKitGTK development package and users need no `libwebkitgtk` installed at runtime.

`exclude` alongside any web declaration is a contradiction, refused with a diagnostic error at validate, build, and package time instead of shipping an app whose declared webviews cannot work:

```zig
.capabilities = .{"webview"},
.webview_layer = "exclude", // rejected: remove the web declarations or drop the exclude
```

## `shell`

The optional `shell` block declares native-first windows with explicit view trees. Existing `windows` entries remain the simple compatibility path; `shell.windows` is the richer contract for native chrome, native controls, WebViews, and GPU surfaces.

Tooling parses and validates the schema. Runtime code can return the same shape from `App.scene_fn`, materialize a parsed shell window with `runtime.createShellWindow(...)`, or attach a shell view list to an existing window with `runtime.createShellViews(...)`; platform hosts implement view kinds progressively, so unsupported native kinds fail at runtime with an explicit unsupported error until that backend grows support.

When an app uses both `windows` and `shell.windows`, labels must stay unique across both lists. Use `windows` for the simple compatibility path or `shell.windows` for native-first structure; do not define two window entries with the same label.

For a scene-first app — a `UiApp` passing its Zig scene (`shell_scene`) to the runner — the scene is authoritative at runtime: it re-applies size, title, and views when it loads. the manifest’s first `shell.windows` entry exists because the host creates the startup window before the scene loads, and create-time-only properties must come from the manifest: `titlebar` chrome, `min_width`/`min_height` floors, overlay presentation (`transparent`, `always_on_top`, `click_through`, and `activate_on_show`), and the show mode (canvas-first windows are created hidden and shown after the first frame presents). The numbers appearing in both places is by design — edit the scene for anything that can change after create (size, title, views), and the manifest for create-time presentation and floors.

```zig
.shell = .{
    .windows = .{
        .{
            .label = "main",
            .title = "Acme",
            .width = 1100,
            .height = 760,
            .views = .{
                .{ .label = "toolbar", .kind = "toolbar", .edge = "top", .height = 44, .role = "toolbar" },
                .{ .label = "body", .kind = "split", .fill = true, .axis = "row" },
                .{ .label = "sidebar", .kind = "sidebar", .parent = "body", .width = 280, .min_width = 220, .max_width = 360 },
                .{ .label = "sidebar-stack", .kind = "stack", .parent = "sidebar", .x = 16, .y = 16, .width = 220, .height = 120, .axis = "column" },
                .{ .label = "sidebar-live", .kind = "checkbox", .parent = "sidebar-stack", .accessibility_label = "Toggle live updates", .text = "Live updates" },
                .{ .label = "content", .kind = "webview", .parent = "body", .url = "zero://app/index.html", .fill = true },
                .{ .label = "canvas", .kind = "gpu_surface", .parent = "body", .width = 480, .gpu_backend = "metal", .gpu_pixel_format = "bgra8_unorm", .gpu_present_mode = "timer", .gpu_alpha_mode = "opaque", .gpu_color_space = "srgb", .gpu_vsync = true },
                .{ .label = "status", .kind = "statusbar", .edge = "bottom", .height = 24, .text = "Ready" },
            },
        },
    },
},
```

Each window takes a `label` plus optional `title`, `width`, `height`, `x`, `y`, `resizable`, `restore_state`, `restore_policy` (`clamp_to_visible_screen` or `center_on_primary`), `initially_hidden` (default false; create the native window ordered out until an explicit show/focus), `allows_fullscreen` (default true; false disables native fullscreen on macOS without disabling ordinary resizing), `min_width`/`min_height` (a content min-size floor the window itself enforces — macOS `contentMinSize`; the first shell window's declaration threads through the startup create like `titlebar`, negative values are a manifest error, 0 means no floor), and `titlebar` (`standard`, `hidden_inset`, `hidden_inset_tall` — the tall variant centers macOS's traffic lights in the 52pt unified band for toolbar-height headers — or `chromeless`, the fully-skinned opt-in that removes all OS chrome including the system buttons; only for apps that draw their own working window controls, see `examples/deck`). `restore_state` defaults to true and controls only whether the state store is consulted: a store hit restores the saved frame, an authored `x` or `y` is explicit placement, and a fresh window with neither is default placement. On macOS the default `clamp_to_visible_screen` policy keeps restored and explicit frames on their matching or nearest display, centers the main fresh window, and cascades fresh secondary windows; `center_on_primary` centers restored and fresh default frames on the primary display. `titlebar = "hidden_inset"` hides the titlebar and extends content under it (macOS keeps the traffic lights) — the first shell window's declaration threads through the STARTUP window create, so the main window's chrome is right from the first frame; the app's own header then takes over dragging and inset padding through the `window-drag` attribute and the `on_chrome` hook (see <a href="/docs/native-ui">Native UI</a>). Platforms without the concept keep standard chrome. The same fields are accepted on top-level `windows` entries. `dock_visible` is top-level app policy, not a window field: setting it false removes the Dock/app-switcher presence but does not hide a window; pair it with `initially_hidden = true` when the app should launch behind its status item.

Overlay presentation is also fixed at create time: `transparent` makes the top-level window alpha-capable, `always_on_top` selects its floating/topmost level, `click_through` passes pointer input to windows underneath, `activate_on_show = false` reveals it without activating the app or taking focus, and `allows_fullscreen = false` removes macOS fullscreen participation while keeping the window resizable. Canvas windows already use present-before-show, so these flags are applied while the window is hidden and its first alpha-correct frame becomes the first visible frame. `initially_hidden = true` is stronger: it suppresses that reveal until `Cmd.showWindow` or explicit focus. Pair `transparent = true` with a non-opaque `gpu_alpha_mode`; `UiApp.WindowDescriptor` makes that canvas-alpha choice and uses an alpha-zero clear automatically. See the <a href="/docs/windows#overlay-windows">overlay window recipe</a>. These fields are accepted on top-level `windows` and `shell.windows`. Runtime `WindowCreateOptions` exposes the same controls but spells the hidden mode `.show = .hidden`; `UiApp.WindowDescriptor` exposes the overlay controls but not the initially-hidden mode.

Windows also take `close_policy` (`quit`, the default — the close affordance really closes, behavior unchanged for every existing app — or `hide`, the menu-bar-app shape: close hides the window and the app keeps running behind its status item). Like `titlebar`, close handling is host window state fixed at create, and the first shell window's declaration threads through the startup create. `hide` is supported on macOS and Windows — on Windows it additionally requires the `"tray"` capability (the tray icon is the only re-show affordance there; a declaration without it is refused at build time with a diagnostic). Linux has no status item to bring a hidden window back, so the declaration is refused at build time with a diagnostic. See <a href="/docs/windows#close-policy">Windows</a> and the <a href="/docs/tray#the-menu-bar-app-lifecycle">tray lifecycle recipe</a>. The same `close_policy` field is accepted on top-level `windows` entries.

Supported `kind` values are `webview`, `toolbar`, `titlebar_accessory`, `sidebar`, `statusbar`, `split`, `stack`, `button`, `icon_button`, `list_item`, `checkbox`, `toggle`, `segmented_control`, `text_field`, `search_field`, `label`, `spacer`, `gpu_surface`, and `progress_indicator`.

Each view has a required `label` and `kind`. WebView views require `url`. Optional layout fields are `parent`, `edge`, `axis`, `x`, `y`, `width`, `height`, `min_width`, `min_height`, `max_width`, `max_height`, `fill`, and `layer`. Optional behavior and accessibility metadata are `visible`, `enabled`, `role`, `accessibility_label`, `text`, and `command`. `gpu_surface` views may also set `gpu_backend`, `gpu_pixel_format`, `gpu_present_mode`, `gpu_alpha_mode`, `gpu_color_space`, and `gpu_vsync`; those fields are rejected on non-GPU view kinds. The macOS system-WebView host presents with Metal, while the Windows host presents representable binary canvas packets with Direct2D/DirectWrite; frame events expose the concrete `metal` or `direct2d` backend. `gpu_backend` accepts `metal` or `software` as portable requests; omit it to select the default. Linux falls back to software presentation, and Windows uses software for unrepresentable commands, transparent layered windows, or when Direct2D is unavailable. The `axis` field accepts `row` or `column` on parent containers such as `toolbar`, `sidebar`, `split`, and `stack`; it defaults to `row`.

`createShellWindow` and `createShellViews` dock `edge` views against the remaining window content, let one or more top-level `fill` views use the final remaining rectangle, clamp resolved frames with any min/max size fields, and lay out parented controls such as toolbar buttons with small native defaults when explicit `x`, `y`, `width`, or `height` values are omitted. Parent containers flow omitted child positions horizontally with `axis = "row"` and vertically with `axis = "column"`. `split` containers use the same axis without inner spacing, so fixed-size children can sit beside a `fill` child. The runtime keeps the shell view slice as a layout binding and reapplies it when the window is resized, so pass data that lives for the lifetime of the window.

## `commands`

The optional `commands` list declares shared command metadata. The runtime still dispatches by command id, but this section gives menus, shortcuts, native controls, tray items, and bridge callers one manifest-level source of truth for user-facing labels and default state:

```zig
.commands = .{
    .{ .id = "app.refresh", .title = "Refresh" },
    .{ .id = "app.sidebar.toggle", .title = "Sidebar", .checked = true },
    .{ .id = "app.sync", .title = "Sync", .enabled = false },
},
```

`id` is required and must be unique. `title` defaults to an empty string. `enabled` defaults to `true`; `checked` defaults to `false`.

An app can define up to 256 commands. Command ids can be up to 128 bytes and titles can be up to 128 bytes.

Generated zero-config TypeScript and Zig-core runners load manifest commands into `RuntimeOptions.commands`; ejected runners use the same fallback. Native code can read the active catalog with `runtime.listCommands(...)`, and trusted WebView code can read it with `window.zero.commands.list()` when the built-in command bridge allows it. Use the catalog to keep menus, shortcuts, toolbar controls, tray items, and bridge callers aligned with the same command ids.

## `shortcuts`

The optional `shortcuts` list defines app-level keyboard shortcuts. Generated zero-config TypeScript and Zig-core runners load these automatically, as do ejected runners:

```zig
.shortcuts = .{
    .{ .id = "command.palette", .key = "p", .modifiers = .{ "primary", "shift" } },
    .{ .id = "reload", .key = "r", .modifiers = .{ "primary" } },
},
```

`id` is the event identifier. `key` accepts letters, digits, the unshifted punctuation keys `=`, `-`, `,`, `.`, `/`, `;`, `'`, `[`, `]`, `\`, and `` ` ``, or a named key. `modifiers` accepts `primary`, `command`, `control`, `option`, `alt`, and `shift`.

Single-character keys and text-entry keys (`enter`, `tab`, `space`, and `backspace`) require at least one modifier.

An app can define up to 64 shortcuts. Shortcut ids can be up to 64 bytes and keys can be up to 32 bytes.

Chromium builds are currently macOS-only; use the Linux system WebView backend when an app needs native shortcut events on Linux.

## `menus`

The optional `menus` list defines native app menus. Generated zero-config TypeScript and Zig-core runners load these automatically, as do ejected runners:

```zig
.menus = .{
    .{
        .title = "View",
        .items = .{
            .{ .label = "Refresh", .command = "app.refresh", .key = "r", .modifiers = .{ "primary" } },
            .{ .separator = true },
            .{ .label = "Sidebar", .command = "app.sidebar.toggle", .checked = true },
        },
    },
},
```

Each menu has a `title` and optional `items`. Command-backed items require `label` and `command`. Optional fields are `key`, `modifiers`, `separator`, `enabled`, and `checked`. `modifiers` accepts the same values as shortcuts.

An app can define up to 16 menus and 128 total menu items. Menu titles can be up to 64 bytes, item labels up to 128 bytes, command ids up to 128 bytes, and keys up to 32 bytes.

## File associations and URL schemes

The optional `file_associations` and `url_schemes` lists declare packaging metadata for document types and custom protocols:

```zig
.file_associations = .{
    .{
        .name = "Markdown Document",
        .role = "editor",
        .extensions = .{ "md", ".markdown" },
        .mime_types = .{ "text/markdown" },
        .icon = "assets/markdown.icns",
    },
},
.url_schemes = .{
    .{ .scheme = "acme-notes" },
},
```

File associations require a `name` and at least one `extensions` or `mime_types` entry. Extensions may include a leading dot, but packaging tools treat the extension as the same value either way. `icon` is optional and must be a relative path.

URL schemes require a lowercase custom `scheme`. Reserved schemes such as `http`, `https`, and `file` are rejected.

`role` defaults to `viewer` and accepts `viewer`, `editor`, `shell`, or `none`. Package generation emits this metadata into macOS `Info.plist` document/protocol declarations, Linux desktop and shared MIME metadata, and a Windows per-user registration script.

## `frontend.dev`

The optional `frontend.dev` block configures the managed dev server for `native dev` and `zig build dev`:

```zig
.frontend = .{
    .dist = "frontend/dist",
    .entry = "index.html",
    .spa_fallback = true,
    .dev = .{
        .url = "http://127.0.0.1:5173/",
        .command = .{ "npm", "--prefix", "frontend", "run", "dev" },
        .ready_path = "/",
        .timeout_ms = 30000,
    },
},
```

<table>
  <thead>
    <tr>
      <th>
        Field
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `url`
      </td>

      <td>
        Dev server URL to load in the WebView during development
      </td>
    </tr>

    <tr>
      <td>
        `command`
      </td>

      <td>
        Command to start the dev server (spawned as a child process)
      </td>
    </tr>

    <tr>
      <td>
        `ready_path`
      </td>

      <td>
        HTTP path to poll until the dev server is ready (default

        `/`

        )
      </td>
    </tr>

    <tr>
      <td>
        `timeout_ms`
      </td>

      <td>
        Milliseconds to wait for the dev server before failing (default

        `30000`

        )
      </td>
    </tr>
  </tbody>
</table>

## Validation

```bash
native validate app.json
native doctor --manifest app.json --strict
```

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Automation
url: "https://native-sdk.dev/docs/automation"
docs_index: /llms.txt
lastUpdated: 2026-10-19
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The automation server exposes runtime state and accepts commands via a file-based protocol. A running app publishes an accessibility snapshot — every widget with its id, role, name, bounds, and state — and accepts scripted clicks, keys, drags, and assertions against it. Use it for integration testing, CI smoke tests, inspecting running apps, and letting AI agents verify their own UI work.

## Enabling automation

Build with the automation flag (`native` verbs forward `-D` flags to the underlying `zig build`):

```bash
native build -Dautomation=true
```

In your runner, pass an `automation.Server` to `RuntimeOptions`:

```zig
const server = native_sdk.automation.Server.init(io, ".zig-cache/native-sdk-automation", "My App");
var runtime = native_sdk.Runtime.init(.{
    .platform = my_platform,
    .automation = server,
});
```

The default directory is `.zig-cache/native-sdk-automation`.

## File protocol

When the runtime publishes a snapshot, it writes these files to the automation directory:

<table>
  <thead>
    <tr>
      <th>
        File
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `snapshot.txt`
      </td>

      <td>
        Runtime state: source kind, window metadata, native/WebView metadata including role, accessibility label, text, and focus state, the configured command and app-menu catalogs,

        `ready=true/false`

        , and

        `markup_watch=armed|off`

        in the header — whether the markup hot-reload watch is armed (only in builds where the app wired

        `.markup`

        with a

        `watch_path`

        and

        `io`

        , or registered compiled fragments through

        `fragment_watch`

        — i.e. Debug dev builds)
      </td>
    </tr>

    <tr>
      <td>
        `accessibility.txt`
      </td>

      <td>
        Accessibility tree summary with native view roles and accessible names. An explicit

        `label=`

        REPLACES the visible text as the accessible name — snapshot greps and screen readers see the label, not the text, so don't label an element whose visible text your assertions grep for.
      </td>
    </tr>

    <tr>
      <td>
        `windows.txt`
      </td>

      <td>
        Window list:

        <code>window @w\{id} "\{title}" focused=\{bool}</code>

        per line
      </td>
    </tr>

    <tr>
      <td>
        `screenshot-<view-label>.png`
      </td>

      <td>
        Deterministic PNG of a

        `gpu_surface`

        view, rendered through the CPU reference renderer on

        `screenshot <view-label> [scale]`
      </td>
    </tr>

    <tr>
      <td>
        `command-<n>.txt`
      </td>

      <td>
        Command queue: one entry per command, written by the CLI, consumed oldest-first by the runtime (which deletes the entry as its consumption ack)
      </td>
    </tr>

    <tr>
      <td>
        `bridge-response.txt`
      </td>

      <td>
        JSON response from the last bridge command
      </td>
    </tr>

    <tr>
      <td>
        `provenance.txt`
      </td>

      <td>
        Response to the last

        `provenance`

        command: where a live widget was authored (file, byte span, line:column, template chain, iteration keys)
      </td>
    </tr>
  </tbody>
</table>

## Commands

The runtime watches the command queue and processes these actions:

<table>
  <thead>
    <tr>
      <th>
        Action
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `reload`
      </td>

      <td>
        Reload the WebView source
      </td>
    </tr>

    <tr>
      <td>
        `wait`
      </td>

      <td>
        Block until the snapshot shows

        `ready=true`
      </td>
    </tr>

    <tr>
      <td>
        `resize <width> <height> [<scale>]`
      </td>

      <td>
        Dispatch a main-window resize event and relayout native/WebView surfaces
      </td>
    </tr>

    <tr>
      <td>
        `provenance <view-label> (<widget-id> | at <x> <y>)`
      </td>

      <td>
        Report where a live widget was authored into

        `provenance.txt`

        : markup file, byte span, line:column, template use-site chain, and iteration keys. Point queries hit-test view-local coordinates.
      </td>
    </tr>

    <tr>
      <td>
        `bridge <json>`
      </td>

      <td>
        Send a bridge command with origin

        `zero://inline`
      </td>
    </tr>

    <tr>
      <td>
        `menu-command <id>`
      </td>

      <td>
        Dispatch a menu command event for the main window
      </td>
    </tr>

    <tr>
      <td>
        `native-command <id> [<view-label>]`
      </td>

      <td>
        Dispatch a native view command event for the main window
      </td>
    </tr>

    <tr>
      <td>
        `widget-action <view-label> <widget-id> <action> [<value>]`
      </td>

      <td>
        Invoke a retained canvas widget action
      </td>
    </tr>

    <tr>
      <td>
        `widget-click <view-label> <widget-id>`
      </td>

      <td>
        Dispatch pointer down/up at a retained canvas widget
      </td>
    </tr>

    <tr>
      <td>
        `widget-hold <view-label> <widget-id>`
      </td>

      <td>
        Press-and-hold: pointer down, the reserved hold timer fired, then the suppressed release — the widget's

        `on_hold`

        Msg through the real gesture path
      </td>
    </tr>

    <tr>
      <td>
        `widget-context-press <view-label> <widget-id>`
      </td>

      <td>
        Secondary click (right/ctrl-click): presents the widget's context menu, or dispatches

        `on_hold`

        immediately when the route has none
      </td>
    </tr>

    <tr>
      <td>
        `widget-context-menu <view-label> <widget-id> <item-index>`
      </td>

      <td>
        Invoke a declared context-menu item by 0-based index (as the snapshot's

        `context_menu=[...]`

        lists them) — the selection dispatches as the same

        `context_menu_action`

        platform event a real pick produces; presentation is skipped because the OS menu's tracking loop cannot be driven. Refuses undeclared menus, out-of-range indices, separators, and disabled items by name
      </td>
    </tr>

    <tr>
      <td>
        `widget-drag <view-label> <widget-id> <start-x-ratio> <end-x-ratio> [<start-y-ratio> <end-y-ratio>]`
      </td>

      <td>
        Dispatch pointer down/drag/up across a retained canvas widget
      </td>
    </tr>

    <tr>
      <td>
        `widget-wheel <view-label> <widget-id> <delta-y> [<delta-x>]`
      </td>

      <td>
        Dispatch wheel input at a retained canvas widget (the optional

        `delta-x`

        scrolls the horizontal axis)
      </td>
    </tr>

    <tr>
      <td>
        `widget-key <view-label> <key> [<text>]`
      </td>

      <td>
        Dispatch key input to the focused retained canvas widget
      </td>
    </tr>

    <tr>
      <td>
        `widget-pinch <view-label> <scale> [<x> <y>]`
      </td>

      <td>
        Dispatch a trackpad pinch gesture at a gpu-surface view: the real

        `pinch_begin`

        /

        `pinch_change`

        /

        `pinch_end`

        events, with one change carrying

        `scale - 1`

        .

        `scale`

        is the final multiplicative zoom for the gesture (1.5 zooms in 50%, 0.5 zooms out to half) — the cumulative product of

        `1 + delta`

        lands exactly on it. The anchor point defaults to the view center
      </td>
    </tr>

    <tr>
      <td>
        `shortcut <id>`
      </td>

      <td>
        Dispatch a shortcut command event for the main window
      </td>
    </tr>

    <tr>
      <td>
        `tray-action <item-id>`

        or

        `tray-action <status-item-id> <item-id>`
      </td>

      <td>
        Select a status-item dropdown row. The one-id form targets primary status item

        `#1`

        ; multiple-item snapshots print

        `tray #id`

        headers
      </td>
    </tr>

    <tr>
      <td>
        `focus <view-label>`
      </td>

      <td>
        Focus a native or WebView-backed view in the main window
      </td>
    </tr>

    <tr>
      <td>
        `focus-next`

        /

        `focus-previous`
      </td>

      <td>
        Move focus through visible, enabled views in runtime order
      </td>
    </tr>

    <tr>
      <td>
        `profile <on|off>`
      </td>

      <td>
        Toggle per-stage frame timing. While on, the snapshot carries a

        `frame_profile`

        line with rolling p50/p90/max microseconds per pipeline stage (

        `rebuild`

        ,

        `layout`

        ,

        `reconcile`

        ,

        `emit`

        ,

        `a11y`

        ,

        `plan`

        ,

        `patch`

        ,

        `encode`

        ,

        `present`

        ,

        `host_decode`

        ,

        `host_draw`

        )
      </td>
    </tr>
  </tbody>
</table>

Commands queue as `command-<n>.txt` entries: each `native automate <command>` claims the next sequence number exclusively (rapid back-to-back invocations can never overwrite each other), the app consumes one entry per presented frame in strict arrival order, and deleting the entry is the consumption ack. The queue is bounded to a handful of entries — a writer finding it full retries until the app drains a slot and fails loudly (non-zero exit) if it never does. `native automate <command>` prints `delivered <action> -> <dir>` only after the app consumed its entry, so a unresponsive app causes a delivery failure.

Widget verbs and `screenshot` address a `gpu_surface` view by its label across ALL open windows — snapshots enumerate every window's views and widgets, so a model-declared secondary window's canvas (a settings window) is drivable and capturable exactly like the main one, with no window argument.

## CLI usage

The `native automate` subcommand interacts with the automation directory:

```bash
# Wait for the app to be ready (polls snapshot.txt for ready=true)
native automate wait

# Assert on the snapshot: each argument is a regex that must match
# (polls up to --timeout-ms, default 30000; --absent inverts)
native automate assert 'gpu_nonblank=true' 'role=button name="Reset"'
native automate assert --absent 'error event='

# List running automation-enabled apps
native automate list

# Dump the current snapshot
native automate snapshot

# Render a gpu_surface view to screenshot-main-canvas.png
native automate screenshot main-canvas

# Reload the WebView
native automate reload

# Resize the main window surface
native automate resize 900 640

# Dispatch command-source events
native automate menu-command app.refresh
native automate native-command app.refresh refresh-button
native automate shortcut app.refresh

# Drive retained canvas widgets
native automate widget-action canvas 2 press
native automate widget-click canvas 3
native automate widget-hold canvas 3            # press-and-hold (on_hold)
native automate widget-context-press canvas 3   # right-click (presents the menu)
native automate widget-context-menu canvas 3 1  # pick declared menu item 1

# Drive focus between native controls and WebViews
native automate focus refresh-button
native automate focus-next
native automate focus-previous

# Toggle per-stage frame timing; the snapshot then carries a
# frame_profile line (rolling p50/p90/max us per pipeline stage)
native automate profile on
native automate snapshot | grep -o 'frame_profile.*'
native automate profile off

# Send a bridge command and get the response
native automate bridge '{"id":"1","command":"native.ping","payload":{"source":"automation"}}'
```

## Testing with automation

The WebView and native-shell smoke build steps demonstrate full automation test flows:

1. Build and start the app with `-Dautomation=true`
2. Run `native automate wait` to block until the app is ready
3. Run `native automate snapshot` to verify window, source, and native/WebView metadata
4. Run `native automate resize ...`, `native automate bridge '...'`, focus traversal, or command-source actions such as `native automate native-command ...`
5. Verify bridge responses, relayout bounds, focus state, and updated snapshots

```bash
zig build test-webview-smoke -Dplatform=macos
```

## Write-back

Because views are data, automation can go the other way too: select a widget in the running app, jump to the markup that authored it, and write an edit back into the source file — the app's own hot-reload watch picks the change up and repaints.

The read half is `provenance`. Every markup-built widget's structural id maps back to its authored source, captured at view build time: the file, the node's byte span and line:column, the template instantiation chain (a widget inside a template reports both its definition site and every `<use>` that put it there), and the iteration keys that say which `<for>` row it is. Widgets built with the Zig builder report `authored=zig` — write-back edits markup files only. TypeScript apps report provenance from their compiled views in `native dev`, and an edit reloads in place through the same dev loop.

```bash
# Where does this widget come from? (id from the snapshot, or hit-test a point)
native automate provenance kanban-canvas 6624116744891006388
native automate provenance kanban-canvas at 760 30
```

The write half is `edit`: typed, minimal-diff operations on the file the widget came from. An operation changes only bytes inside the target node's span — whitespace, comments, attribute order, and every other node survive byte for byte, proven by reparsing and diffing the parse trees before anything is written.

```bash
native automate edit kanban-canvas 6624116744891006388 set-text "Add task"
native automate edit kanban-canvas 6624116744891006388 set-attr variant secondary
native automate edit kanban-canvas 6624116744891006388 remove-attr variant
```

Edits refuse rather than guess: a widget authored in Zig, a file that changed on disk since the app loaded it (the provenance response carries the loaded bytes' hash, so concurrent edits are never clobbered), or an edit that would fail markup validation all stop with a diagnostic and leave the file untouched. A successful edit needs no reload command — the markup watch (`MarkupOptions.watch_path`, a dev/Debug feature) reloads within its poll interval, so `native automate assert` on the snapshot is the way to await the repaint.

`zig build test-writeback-smoke` (macOS) drives the whole loop against the kanban example: query provenance, flip the button label through the verb, assert the repaint, verify the byte-exact diff, and flip it back.

## Session recording and blob growth

`NATIVE_SDK_SESSION_RECORD=/path/session.journal` records platform events and effect results; `NATIVE_SDK_SESSION_REPLAY` replays that journal without touching the original network, process, database, or file. Large effect payloads—including every streamed file-read chunk—live content-addressed under the sibling `blobs/` directory, and the journal stores each hash and length. Keep the journal and `blobs/` together when copying a recording.

The blob store deduplicates identical chunks but is intentionally unbounded today: there is no automatic size quota or garbage collector. Long recordings and repeated large imports can therefore grow the directory substantially. Treat a recording as one disposable artifact, monitor its directory size in long automation runs, and delete the journal plus its sibling `blobs/` directory when it is no longer needed.

## Custom directory

Pass a custom path to `automation.Server.init()`:

```zig
const server = native_sdk.automation.Server.init(io, "/tmp/my-app-automation", "My App");
```

The CLI reads from the default `.zig-cache/native-sdk-automation` unless you specify a directory via the automation subcommand.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Building Components
url: "https://native-sdk.dev/docs/building-components"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use [theming](/docs/theming) to customize the built-in components. App components are Native markup templates: they compose built-ins, accept values through template args, accept content through slots, and live in `.native` component files. Component logic stays in the TypeScript core's ordinary `update` and derived helpers. The mechanics (template grammar, import rules, slots) are specified in [Native UI](/docs/native-ui#templates); this page builds one real component end to end.

The ownership model in one line: **use and theme the built-ins by default; eject a library composite when you need to own its shape; build new composites from primitives when the library has no shape for it.** The last two are this page.

## A component in markup

A dashboard needs the same labeled stat tile three times. That repetition is a `<template>` — define the subtree once at the top of the view file, name its parameters in `args`, and instantiate it with `<use>`:

```html
<template name="stat-card" args="label value">
  <column background="surface" border-color="border" radius="lg" padding="12" gap="4" role="group" label="{label}">
    <text size="sm" foreground="text_muted">{label}</text>
    <text size="heading">{value}</text>
  </column>
</template>
<column gap="12" padding="16">
  <row gap="12">
    <use template="stat-card" label="CPU" value="{cpuPercent}" />
    <use template="stat-card" label="Memory" value="{memoryUsed}" />
  </row>
</column>
```

Inside the body, args bind exactly like model bindings: `{label}` in text, `label="{label}"` in attributes. At the use site an arg's value may be a literal (`label="CPU"`), a binding (`value="{cpuPercent}"`), or an expression — and the attributes must match the declared args exactly, so a typo is a check error, never a silently ignored prop. The body is built in place of the `<use>`: widget ids hash as if you had written it out by hand, which means converting copy-pasted markup into a template changes no identity, no retained state, no automation target.

Optional args declare a literal default with `name=value` (`args="label value hint="` gives `hint` an empty-string default), and the body can branch on them — this is how the card grows an optional footnote without a second template:

```html
<template name="stat-card" args="label value hint=">
  <column background="surface" border-color="border" radius="lg" padding="12" gap="4" role="group" label="{label}">
    <text size="sm" foreground="text_muted">{label}</text>
    <text size="heading">{value}</text>
    <if test="{hint}">
      <text size="sm" foreground="text_muted">{hint}</text>
    </if>
  </column>
</template>
```

## Moving it to a component file

Once a second view wants the card, move the template into its own file. A file of templates and nothing else is a **component file** — it has no view root, and that is what makes it importable:

```html
<!-- src/components/stat-card.native -->
<template name="stat-card" args="label value hint=">
  <column background="surface" border-color="border" radius="lg" padding="12" gap="4" role="group" label="{label}">
    <text size="sm" foreground="text_muted">{label}</text>
    <text size="heading">{value}</text>
    <if test="{hint}">
      <text size="sm" foreground="text_muted">{hint}</text>
    </if>
  </column>
</template>
```

The view imports it at the very top, before anything else, and uses it exactly as before:

```html
<import src="components/stat-card.native"/>
<column gap="12" padding="16" background="background">
  <row gap="12">
    <use template="stat-card" label="CPU" value="{cpuPercent}" hint="last minute" />
    <use template="stat-card" label="Memory" value="{memoryUsed}" />
    <use template="stat-card" label="Uptime" value="{uptimeText}" />
  </row>
</column>
```

Validate as you go: `native markup check src/app.native` follows the import closure from disk, and a component file also checks standalone (`native markup check src/components/stat-card.native`). `native check` walks everything under `src/` in one pass.

## Passing children with a slot

Args carry values; a `<slot/>` carries markup. A template body may mark one insertion point, and the use site's children build there — in the consumer's scope, so they see the model paths and loop variables where the `<use>` is written:

```html
<template name="empty-state" args="title">
  <column main="center" cross="center" gap="8" padding="24" role="group" label="{title}">
    <text foreground="text_muted">{title}</text>
    <slot/>
  </column>
</template>
<column grow="1">
  <use template="empty-state" title="No results yet">
    <button variant="primary" on-press="refresh">Refresh</button>
  </use>
</column>
```

This is the container-component pattern: the template owns the frame, the caller owns the content. The full rules (one slot per body, children without a slot are an error, ids hash as if inlined) are in [Native UI § Components](/docs/native-ui#components).

## Put component logic in the core

Templates own structure, not state. Keep state transitions in `update`, expose reusable derived values as exported TypeScript helpers, and bind those values into the component. A component never runs an app callback while the view builds.

When the closed markup vocabulary does not expose a lower-level engine capability yet, there is no app-side escape element or required Zig file. Use the nearest built-in composition and track the missing grammar admission. Toolkit extensions remain an SDK implementation concern.

## Theming your component

Use design-token references to make a component follow the active theme.

In markup, the style attributes (`background`, `foreground`, `border-color`, `radius`, ...) are token **references** — the stat card above says `background="surface"`, not a hex value. References resolve against the app's live tokens on every rebuild, so the card follows dark mode, a theme-pack switch, and every override with zero component code. Unknown token names are check/compile errors.

State washes follow the built-in controls' behavior. Markup components customize their token references and compose the appropriate control kinds. A Zig view extension may use `ElementOptions.style` for a state-specific visual the token vocabulary does not expose; keep that exception local to the extension.

Built-in controls inside the template continue to use their own tokens. For example, a primary button reads `controls.button_primary`, and text reads the typography tokens. Prefer token references for visuals that should change with the app’s theme.

## Distributing components across an app

Component files live wherever you like under the markup root (the root view file's directory) — `src/components/` is the convention the tooling writes to, and subdirectories work (`<import src="components/cards/stat-card.native"/>`). Paths are relative to the importing file, imports are transitive, and everything must stay under the markup root. Cycles are reported with the chain spelled out, and the same template name defined in two files is an error naming both sites — imports never shadow silently.

The generated TypeScript app wiring handles both engine paths:

- The runtime interpreter re-resolves imports from disk on hot reload, so `native dev` picks up edits to component files, not just the root view.
- Release and mobile builds embed every `.native` file under `src/` except the root. App code does not maintain an embed registry.

## Use, eject, or build

Choose an approach based on what needs to change:

**Theme it.** Every built-in reads the token register, and [theme packs, overrides, and full design systems](/docs/theming) reach every visual decision — colors, radii, control metrics, state washes, type. If your need is "the stepper, but in our palette," that is a token change, not a component.

**Eject it.** When you need to own a composite's *shape* — reorder its parts, change its structure, grow it a feature — `native eject component <name>` writes the library composite's canonical source into `src/components/`, and from then on it is your code: edit freely, SDK updates never touch it. Markup templates are reached through `<use template="...">`, never a new element name, so the built-in element keeps working at unmigrated call sites. Ejecting twice errors instead of overwriting your edits. Engine controls (buttons, text fields, tabs, ...) are deliberately not on the menu: their behavior lives in the runtime, and the way to change them is the token system, not a fork.

**Build it.** When the library has no shape for it, compose one from markup primitives — the stat card and empty-state above. Your component then themes, tests, and automates exactly like a built-in, because it is made of the same parts.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Built-in Components
url: "https://native-sdk.dev/docs/built-in-components"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Browse the [component catalog](/docs/components) for Native markup examples, rendered previews, Zig builder equivalents, and attribute references. For layout and bindings, see [Native UI](/docs/native-ui).

Zig code can inspect the catalog through `native_sdk.canvas.builtin_component_kinds` and `builtin_component_names`. Each descriptor includes its style, root widget kind, semantic role, and composite flag. `builtinComponentWidget(...)` constructs a component’s default widget.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Capabilities
url: "https://native-sdk.dev/docs/capabilities"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Native SDK capabilities are native OS services and app events exposed through `PlatformServices`, runtime methods, lifecycle events, and — for apps that [embed web content](/docs/frontend) — guarded bridge commands. Native code reaches them directly; web content does not receive capability access by default. In a [model core](/docs/app-model), OS work rides the effects channel (`Cmd.*` in TypeScript and `fx.*` in Zig) so `update` never needs a runtime handle.

Web content itself is declare-to-use: an app ships the embedded web layer only when it declares web intent — `"webview"` in `.capabilities`, a `.frontend` block, a `.shell` webview view, or a web engine resolved to Chromium (`.web_engine = "chromium"` in app.zon, or the `-Dweb-engine`/`--web-engine` flags) — and an app that declares none of them builds native-only, where any attempt to create a webview fails with a teaching error instead of loading a layer the app never asked for. The [`webview_layer`](/docs/app-zon) manifest field overrides the inference in either direction. Native-only builds shed the platform web stack for real: the Windows executable carries no `WebView2Loader.dll` reference, and the Linux host neither links WebKitGTK nor requires `libwebkitgtk` on user machines.

## Current capability pack

<table>
  <thead>
    <tr>
      <th>
        Capability
      </th>

      <th>
        Native API
      </th>

      <th>
        JavaScript command
      </th>

      <th>
        Bridge permission
      </th>

      <th>
        Current native support
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        Open URL in system browser
      </td>

      <td>
        `Cmd.openExternalUrl(url)`

        /

        `runtime.openExternalUrl(url)`
      </td>

      <td>
        `native-sdk.os.openUrl`
      </td>

      <td>
        `network`
      </td>

      <td>
        macOS, Linux, and Windows model cores and system WebView; macOS Chromium
      </td>
    </tr>

    <tr>
      <td>
        System notification
      </td>

      <td>
        `Cmd.showNotification(spec)`

        /

        `fx.showNotification(options)`

        /

        `runtime.showNotification(options)`
      </td>

      <td>
        `native-sdk.os.showNotification`
      </td>

      <td>
        `notifications`
      </td>

      <td>
        macOS, Linux, and Windows model cores and system WebView; macOS Chromium
      </td>
    </tr>

    <tr>
      <td>
        Model persistence
      </td>

      <td>
        `Cmd.persist()`

        /

        `fx.persist()`
      </td>

      <td>
        None. Model-core effect only.
      </td>

      <td>
        None. Gated by the

        `persist`

        build capability.
      </td>

      <td>
        Generated TypeScript app runners on every app-data platform; Zig-core hosts receive the same named

        `core.persist`

        effect seam
      </td>
    </tr>

    <tr>
      <td>
        Record store
      </td>

      <td>
        `Cmd.store.set/get/delete/scan/setMany`

        /

        `fx.storeSet/storeGet/storeDelete/storeScan/storeSetMany`
      </td>

      <td>
        None. Model-core effect only.
      </td>

      <td>
        None. Gated by the

        `store`

        build capability.
      </td>

      <td>
        SQLite-backed engine store in the per-app data directory; replay remains offline and the core devhost uses a process-local map
      </td>
    </tr>

    <tr>
      <td>
        Relational SQLite
      </td>

      <td>
        `Cmd.q<Name>`

        /

        `Cmd.qTx`

        /

        `Sub.q<Name>`

        / raw

        `Cmd.db.query/exec`

        /

        `fx.dbQuery/dbExec/dbSubscribe`
      </td>

      <td>
        None. Model-core effect only.
      </td>

      <td>
        None. Gated by the

        `sqlite`

        build capability.
      </td>

      <td>
        Checked migrations and named SQL over engine-owned

        `app.db`

        ; real in-memory SQLite in check, devhost, and tests; journal-only replay
      </td>
    </tr>

    <tr>
      <td>
        Native dialogs
      </td>

      <td>
        `runtime.showOpenDialog(...)`

        /

        `showSaveDialog(...)`

        /

        `showMessageDialog(...)`
      </td>

      <td>
        `native-sdk.dialog.openFile`

        /

        `saveFile`

        /

        `showMessage`
      </td>

      <td>
        `dialog`
      </td>

      <td>
        macOS system WebView and Chromium; Linux and Windows system WebView
      </td>
    </tr>

    <tr>
      <td>
        Reveal path in file manager
      </td>

      <td>
        `Cmd.revealPath(path)`

        /

        `runtime.revealPath(path)`
      </td>

      <td>
        `native-sdk.os.revealPath`
      </td>

      <td>
        `filesystem`
      </td>

      <td>
        macOS, Linux, and Windows model cores and system WebView; macOS Chromium
      </td>
    </tr>

    <tr>
      <td>
        Recent documents
      </td>

      <td>
        `runtime.addRecentDocument(path)`

        /

        `runtime.clearRecentDocuments()`
      </td>

      <td>
        `native-sdk.os.addRecentDocument`

        /

        `native-sdk.os.clearRecentDocuments`
      </td>

      <td>
        `filesystem`
      </td>

      <td>
        macOS, Linux, and Windows system WebView; macOS Chromium
      </td>
    </tr>

    <tr>
      <td>
        Clipboard
      </td>

      <td>
        `runtime.readClipboard()`

        /

        `runtime.writeClipboard(text)`

        /

        `runtime.readClipboardData(mimeType)`

        /

        `runtime.writeClipboardData(data)`
      </td>

      <td>
        `native-sdk.clipboard.readText`

        /

        `native-sdk.clipboard.writeText`

        /

        `native-sdk.clipboard.read`

        /

        `native-sdk.clipboard.write`
      </td>

      <td>
        `clipboard`
      </td>

      <td>
        `text/plain`

        on supported clipboard hosts;

        `text/html`

        and RTF on macOS system WebView, macOS Chromium, Linux system WebView, and Windows system WebView
      </td>
    </tr>

    <tr>
      <td>
        Audio playback
      </td>

      <td>
        `fx.playAudio(options)`

        /

        `fx.pauseAudio()`

        /

        `fx.resumeAudio()`

        /

        `fx.stopAudio()`

        /

        `fx.seekAudio(ms)`

        /

        `fx.setAudioVolume(v)`
      </td>

      <td>
        None. Effects-channel API; every report arrives as an

        `on_event`

        Msg.
      </td>

      <td>
        None. No bridge surface.
      </td>

      <td>
        macOS system-engine host (ONE AVPlayer for local files, verified cache entries, and streamed URLs — one tappable path), Windows system WebView (one Media Foundation media session for local files and streamed URLs), Linux system WebView (one GStreamer playbin for local files and streamed URLs, loaded at runtime — a host without GStreamer reports unsupported), the iOS toolkit host (the AVFoundation player behind the embed audio service, with an playback audio session — interruptions pause and report), and the Android toolkit host (one MediaPlayer for local files, verified cache entries, and progressive URL streams behind the same embed audio service, with audio focus — a focus loss pauses and reports); macOS Chromium and GStreamer-less Linux hosts report unsupported and playback degrades to one explicit failed Msg
      </td>
    </tr>

    <tr>
      <td>
        Audio spectrum analysis
      </td>

      <td>
        None to call — hosts that analyze deliver

        `.spectrum`

        audio events through the same

        `on_event`

        Msg as position ticks
      </td>

      <td>
        None. Effects-channel events.
      </td>

      <td>
        None. No bridge surface.
      </td>

      <td>
        macOS system-engine host (an MTAudioProcessingTap on the app's single AVPlayer feeding a vDSP FFT — local files, cache entries, and streams alike), Windows system WebView (process-scoped WASAPI loopback capture of THIS app's audio session only — never other apps' audio — with an in-box FFT), and Linux system WebView (GStreamer's

        `spectrum`

        element as the playbin's audio-filter; requires gst-plugins-good, probed live); hosts that cannot analyze — macOS Chromium, pre-2004 Windows, spectrum-less GStreamer setups, iOS, Android — report

        `audio_spectrum`

        unsupported and simply never send the events: no spectrum events
      </td>
    </tr>

    <tr>
      <td>
        File drops
      </td>

      <td>
        `Event.files_dropped`
      </td>

      <td>
        `window.zero.on("drop:files")`
      </td>

      <td>
        None. Host-emitted event.
      </td>

      <td>
        macOS, Linux, and Windows system WebView
      </td>
    </tr>

    <tr>
      <td>
        Credential store
      </td>

      <td>
        `Cmd.credentials.set/get/delete`

        /

        `fx.credentialsSet/Get/Delete`

        / runtime equivalents
      </td>

      <td>
        `native-sdk.credentials.set`

        /

        `native-sdk.credentials.get`

        /

        `native-sdk.credentials.delete`
      </td>

      <td>
        `credentials`

        . Model cores require both the build capability and permission; WebView commands require the bridge permission.
      </td>

      <td>
        macOS model cores, system WebView, and Chromium through Keychain; Linux model cores and system WebView through Secret Service/libsecret when available; Windows model cores and system WebView through Credential Manager; iOS toolkit apps through generic-password Keychain entries; Android toolkit apps through an AndroidKeyStore AES-GCM key and authenticated ciphertext in app-private preferences. Core dev and test hosts use hermetic memory.
      </td>
    </tr>

    <tr>
      <td>
        Local date/time formatting
      </td>

      <td>
        `Cmd.formatLocalTime(timestampMs, style, route)`

        /

        `runtime.formatLocalTime(...)`
      </td>

      <td>
        None. Effects/runtime API.
      </td>

      <td>
        None. No bridge surface.
      </td>

      <td>
        macOS, Linux, and Windows model cores; locale- and time-zone-aware, with results captured by session recording
      </td>
    </tr>

    <tr>
      <td>
        App activation events
      </td>

      <td>
        `LifecycleEvent.activate`

        /

        `LifecycleEvent.deactivate`
      </td>

      <td>
        `window.zero.on("app:activate")`

        /

        `window.zero.on("app:deactivate")`
      </td>

      <td>
        None. Host-emitted event.
      </td>

      <td>
        macOS, Linux, and Windows system WebView; macOS Chromium
      </td>
    </tr>

    <tr>
      <td>
        File associations and URL schemes
      </td>

      <td>
        `Manifest.file_associations`

        /

        `Manifest.url_schemes`
      </td>

      <td>
        None. Packaging metadata.
      </td>

      <td>
        None. Manifest-only declaration.
      </td>

      <td>
        Parsed, validated, and emitted in macOS, Linux, and Windows package registration metadata.
      </td>
    </tr>
  </tbody>
</table>

Unsupported platform hosts reject with the standard unsupported-service error.

## Audio streaming and the track cache

`fx.playAudio` resolves its source in a fixed order: the local `path` first; when it is absent or missing, the `url` — where a verified cache entry at `cache_path` plays as a plain local file (no network), and anything else streams progressively (playback starts while bytes are still arriving, never download-then-play) while the same bytes fill the cache for the next play. `expected_bytes` (the track's known size, from a manifest) is the integrity gate: a cache entry or a finished download whose size disagrees never plays — it is discarded and re-streamed. While a stream waits for bytes, position events carry `buffering = true`: a buffering state, distinct from playing and paused, since the transport is not paused but nothing is coming out of the speakers. Seek and volume work mid-stream; a network failure mid-stream (or offline with a cold cache) delivers one explicit `.failed` Msg.

The cache lives under the platform caches directory (the `app_dirs` `.cache` convention — `~/Library/Caches/<app>/audio/` on macOS, `$XDG_CACHE_HOME/<app>/audio/` on Linux, `%LOCALAPPDATA%\<app>\Cache\audio\` on Windows, the app sandbox container's `Library/Caches/audio/` on iOS and the app data directory's `cache/audio/` on Android — both namespaces are already per-app, and both systems treat those directories as reclaimable), keyed by the URL's hash (`native_sdk.audioCachePath` derives the path). Clearing the cache is deleting that one directory; the OS already treats caches there as reclaimable. Hosts with a player but no streaming path report `audio_streaming` unsupported and URL playback degrades to the same explicit failed Msg. For record/replay, streaming changes nothing: the delivered audio events are journaled at the effect boundary like every other result, so network timing — real-world nondeterminism — stays outside the recording and replay never touches the network.

## Spectrum analysis

Hosts with `audio_spectrum` analyze the audio the app itself is producing and deliver `.spectrum` events through the active playback's ordinary `on_event` Msg: 32 band magnitudes with log-spaced center frequencies covering roughly 50 Hz–16 kHz, each band a byte mapping linearly in decibels from the −60 dBFS analysis floor (0) to full scale (255) — divide by 255 for a 0..1 level, and equal visual steps read as equal dB steps, the way hardware analyzers do. Per band the host reports the peak FFT-bin magnitude inside the bucket. Events arrive at a steady \~25 Hz (40 ms cadence) only while audio is audibly playing: pause and stop starve the stream, and a buffering stall goes quiet with the speakers, so everything a UI derives from the bands stops updating. Silence while playing is a row of floor bytes, still delivered — the cadence follows the transport, the magnitudes tell the truth. The bands also describe a display, so a host that knows the app has no visible window stops delivering them while the glass is away — macOS while every window is occluded (minimized, fully covered, hidden), Windows while every window is minimized or hidden by its `close_policy` — and resumes within one report of reveal; playback and the position ticks continue untouched, and an occluded stretch produces no spectrum events in the journal. Linux keeps emitting regardless: no visibility signal holds across compositors, and a gate that might never clear would freeze visible bars.

The mechanisms are host-native and in-box: macOS taps the app's single AVPlayer with an MTAudioProcessingTap and folds a vDSP FFT into the bands (the tap sits pre-effects, so the bands describe the decoded track, not the app's volume fader); Windows captures the app's OWN audio session through process-scoped WASAPI loopback — never system-wide, so other apps' audio can never pollute the bands — and runs an in-box FFT; Linux installs GStreamer's `spectrum` element as the playbin's audio-filter and folds its linear bands into the documented log buckets. A host that cannot analyze (macOS Chromium, Windows before 10 2004, a GStreamer install without gst-plugins-good, iOS, Android) reports `audio_spectrum` unsupported and never sends the events — apps should show a resting state, not fake motion. For record/replay, the bands are recorded at the effect boundary: every `.spectrum` event journals verbatim at the effect boundary like a position tick, so replay repaints identical bars, and the null platform ships a deterministic fake generator so tests stay hermetic.

## Support discovery

Use `runtime.supports(...)` before native code shows optional affordances. Trusted WebView code can make the same check with `native-sdk.platform.supports` when that built-in command is explicitly allowed:

```javascript
const support = {
  notifications: await window.zero.platform.supports("notifications"),
  dialogs: await window.zero.platform.supports("dialogs"),
  clipboardRichData: await window.zero.platform.supports("clipboard_rich_data"),
  credentials: await window.zero.platform.supports("credentials"),
  fileDrops: await window.zero.platform.supports("file_drops"),
  audioPlayback: await window.zero.platform.supports("audio_playback"),
  audioStreaming: await window.zero.platform.supports("audio_streaming"),
  audioSpectrum: await window.zero.platform.supports("audio_spectrum"),
  microphoneCapture: await window.zero.platform.supports("microphone_capture"),
  systemAudioCapture: await window.zero.platform.supports("system_audio_capture"),
};
```

## Security

OS bridge commands are default-deny. Add explicit `builtin_bridge` entries for each command your app calls:

```zig
.builtin_bridge = .{
    .enabled = true,
    .commands = &.{
        .{ .name = "native-sdk.command.invoke", .permissions = .{ "command" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.platform.supports", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.openUrl", .permissions = .{ "network" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.showNotification", .permissions = .{ "notifications" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.dialog.openFile", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.dialog.saveFile", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.dialog.showMessage", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.revealPath", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.addRecentDocument", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.clearRecentDocuments", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.clipboard.readText", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.clipboard.writeText", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.clipboard.read", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.clipboard.write", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.credentials.set", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.credentials.get", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.credentials.delete", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } },
    },
},
```

Because the JavaScript example below supplies a notification action, the app/runtime permission list must also include `command`. Display-only notifications need only the `native-sdk.os.showNotification` policy and `notifications` permission.

Opening external URLs is also gated by the external-link policy:

```zig
.security = .{
    .navigation = .{
        .external_links = .{
            .action = .open_system_browser,
            .allowed_urls = &.{ "https://example.com/docs/*" },
        },
    },
},
```

## JavaScript

```javascript
await window.zero.os.openUrl("https://example.com/docs/start");
await window.zero.os.showNotification({
  id: "build-status",
  title: "Build finished",
  subtitle: "native-sdk",
  body: "All checks passed.",
  actionLabel: "Open results",
  actionCommand: "build.open-results",
});
await window.zero.os.revealPath("/Users/me/Downloads/report.pdf");
await window.zero.os.addRecentDocument("/Users/me/Downloads/report.pdf");
await window.zero.os.clearRecentDocuments();

await window.zero.clipboard.writeText("Copied from Native SDK");
const text = await window.zero.clipboard.readText();
await window.zero.clipboard.write({
  mimeType: "text/html",
  data: "<strong>Copied from Native SDK</strong>",
});
const html = await window.zero.clipboard.read({ mimeType: "text/html" });

window.zero.on("drop:files", (event) => {
  console.log(event.paths);
});

await window.zero.credentials.set({
  service: "com.example.app",
  account: "current-user",
  secret: "session-token",
});
const token = await window.zero.credentials.get({
  service: "com.example.app",
  account: "current-user",
});
await window.zero.credentials.delete({
  service: "com.example.app",
  account: "current-user",
});

window.zero.on("app:activate", () => {
  refreshForegroundState();
});
```

Use the app manifest’s id as `service` when WebView code and a model core need to address the same entry. Core credential effects apply that namespace automatically; their key corresponds to the bridge `account` field.

## Model cores

Model-driven apps request notifications as effects. Delivery is fire-and-forget because OS focus modes and user settings remain authoritative after the host accepts the request; fake-executor tests and session replay do not display notifications. An optional nonempty `id` replaces the app's earlier notification with the same id. Supply `actionLabel` and `actionCommand` together to route activation through the same application command path as menus, shortcuts, and bridge command invocation. Action-command delivery is process-scoped: a notification from an earlier execution cannot dispatch its command into a later one. Linux notification shells may still D-Bus-activate an exited app when the notification or its action is clicked, but the stale action is rejected rather than entering the new runtime.

### TypeScript

```ts
case "build_finished":
  return [model, Cmd.showNotification({
    id: asciiBytes("build-status"),
    title: asciiBytes("Build finished"),
    subtitle: asciiBytes("native-sdk"),
    body: asciiBytes("All checks passed."),
    actionLabel: asciiBytes("Open results"),
    actionCommand: asciiBytes("build.open-results"),
  })];
```

### Zig

```zig
.build_finished => fx.showNotification(.{
    .id = "build-status",
    .title = "Build finished",
    .subtitle = "native-sdk",
    .body = "All checks passed.",
    .action_label = "Open results",
    .action_command = "build.open-results",
}),
```

Apps using the lower-level runtime can call the same platform seam directly with `try runtime.showNotification(options)`.

### Credentials

Credentials are app-scoped effects, not Model data. Declare both gates; `native check` reports NS1071 for a missing capability and NS1072 for a missing permission when a TypeScript core uses `Cmd.credentials.*`. NS1073 reserves the underlying `core.credentials.*` request names for these typed factories:

```zig title="app.zon"
.capabilities = .{ "credentials" },
.permissions = .{ "credentials" },
```

The manifest app id is the OS keychain service namespace and is bounded at 128 bytes, so authored code supplies only a key. Keys are NUL-free UTF-8 through 256 bytes and secrets are bounded at 2,560 bytes, the largest binary value every first-party OS backend can store whole. A get miss routes the error arm with `miss`; the other closed outcomes are `denied`, `locked`, `io_failed`, `over_bound`, and `rejected`. Set and delete route empty bytes on success, and delete is idempotent.

```ts
case "save_token":
  return [model, Cmd.credentials.set("api-token", msg.token, {
    key: "save-token",
    ok: "token_saved",
    err: "credential_failed",
  })];
case "load_token":
  return [model, Cmd.credentials.get("api-token", {
    key: "load-token",
    ok: "token_loaded",
    err: "credential_failed",
  })];
case "token_loaded":
  // Consume msg.token immediately to construct the next effect. Do not
  // copy it into Model, where persistence and state fingerprints can see it.
  return [model, Cmd.fetch({
    url: asciiBytes("https://api.example.com/me"),
    headers: { authorization: msg.token },
  }, { ok: "profile_loaded", err: "profile_failed" })];
```

```zig
.save_token => |token| fx.credentialsSet(.{
    .key = 41,
    .credential_key = "api-token",
    .secret = token,
    .on_result = Effects.credentialsMsg(.credential_result),
}),
.load_token => fx.credentialsGet(.{
    .key = 42,
    .credential_key = "api-token",
    .on_result = Effects.credentialsMsg(.credential_result),
}),
```

Credential results still cross the effect boundary, but recording never writes secret bytes to the journal or blob store. A successful recorded get keeps only its length, a per-session salt, and a placeholder digest that is deliberately independent of the secret (so the artifact is not a password-guessing oracle). Replay supplies deterministic placeholder bytes of the same length and never opens the live keychain. Consequently, replay is suitable for control-flow verification but cannot perform a new authenticated exchange with the original token. The core devhost also stores credentials only in process memory and prints `<redacted, N bytes>`.

See also: [Builtin Commands](/docs/bridge/builtin-commands) and [Security](/docs/security).

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Commands
url: "https://native-sdk.dev/docs/commands"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Commands are the shared routing layer for native app actions. Menus, shortcuts, toolbar controls, tray items, native controls, bridge calls, and runtime code can all dispatch the same command name into `Event.command`.

```zig
fn event(context: *anyopaque, runtime: *native_sdk.Runtime, event_value: native_sdk.Event) anyerror!void {
    _ = context;
    switch (event_value) {
        .command => |command| {
            if (std.mem.eql(u8, command.name, "app.refresh")) {
                try runtime.updateView(command.window_id, "status", .{
                    .text = "Refreshed",
                });
            }
        },
        else => {},
    }
}
```

## Sources

`CommandEvent.source` identifies where the action came from:

<table>
  <thead>
    <tr>
      <th>
        Source
      </th>

      <th>
        Emitted by
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `.runtime`
      </td>

      <td>
        Direct

        `runtime.dispatchCommand(...)`

        calls
      </td>
    </tr>

    <tr>
      <td>
        `.menu`
      </td>

      <td>
        Native menu items
      </td>
    </tr>

    <tr>
      <td>
        `.shortcut`
      </td>

      <td>
        Native keyboard shortcuts
      </td>
    </tr>

    <tr>
      <td>
        `.toolbar`
      </td>

      <td>
        Native controls inside a toolbar view
      </td>
    </tr>

    <tr>
      <td>
        `.tray`
      </td>

      <td>
        Tray actions
      </td>
    </tr>

    <tr>
      <td>
        `.native_view`
      </td>

      <td>
        Native controls outside toolbar views
      </td>
    </tr>

    <tr>
      <td>
        `.bridge`
      </td>

      <td>
        `window.zero.commands.invoke(...)`
      </td>
    </tr>
  </tbody>
</table>

Use `command.name` for the action and `command.source` only when the app needs behavior specific to a source. Tray commands also include `tray_item_id` for apps that still need to distinguish legacy tray items that dispatch `"tray.action"`.

## Command names

Command names are stable IDs, not display labels. Use namespaced lowercase IDs:

```zig
const shortcuts = [_]native_sdk.Shortcut{
    .{ .id = "app.refresh", .key = "r", .modifiers = .{ .primary = true } },
};

const view_items = [_]native_sdk.MenuItem{
    .{ .label = "Refresh", .command = "app.refresh", .key = "r", .modifiers = .{ .primary = true } },
};
```

The same command can be bound to a menu item, shortcut, toolbar button, tray item, and bridge call. The runtime validates command IDs and rejects empty, oversized, or path-like names.

`app.zon` can also declare shared command metadata:

```zig
.commands = .{
    .{ .id = "app.refresh", .title = "Refresh" },
    .{ .id = "app.sidebar.toggle", .title = "Sidebar", .checked = true },
},
```

This metadata gives native and web entry points a common manifest-level command catalog while runtime dispatch still happens through `Event.command`.

Generated runners load that catalog into runtime options. Native code can read the active commands without reparsing the manifest:

```zig
var buffer: [32]native_sdk.Command = undefined;
const commands = runtime.listCommands(&buffer);
for (commands) |command| {
    if (command.enabled) {
        // Bind command.id to native controls or custom UI.
    }
}
```

## Bridge invocation

When `js_window_api` and the built-in bridge policy allow it, WebView content can dispatch the same command:

```js
await window.zero.commands.invoke("app.refresh");
```

The resulting event has `source = .bridge`, the calling native `window_id`, and the calling view label when the call came from a named child WebView.

WebView content can also read the manifest command catalog:

```js
const commands = await window.zero.commands.list();
```

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Dialogs
url: "https://native-sdk.dev/docs/dialogs"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The Native SDK provides native file and message dialogs accessible from Zig via `Runtime.showOpenDialog(...)`, `Runtime.showSaveDialog(...)`, and `Runtime.showMessageDialog(...)`, or from JavaScript via the [builtin bridge](/docs/bridge/builtin-commands).

## Open file dialog

<table>
  <thead>
    <tr>
      <th>
        Field
      </th>

      <th>
        Type
      </th>

      <th>
        Default
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `title`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `""`
      </td>
    </tr>

    <tr>
      <td>
        `default_path`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `""`
      </td>
    </tr>

    <tr>
      <td>
        `filters`
      </td>

      <td>
        `[]const FileFilter`
      </td>

      <td>
        <code>&.</code>
      </td>
    </tr>

    <tr>
      <td>
        `allow_directories`
      </td>

      <td>
        `bool`
      </td>

      <td>
        `false`
      </td>
    </tr>

    <tr>
      <td>
        `allow_multiple`
      </td>

      <td>
        `bool`
      </td>

      <td>
        `false`
      </td>
    </tr>
  </tbody>
</table>

Returns `OpenDialogResult` with `count` and `paths`.

## Save file dialog

<table>
  <thead>
    <tr>
      <th>
        Field
      </th>

      <th>
        Type
      </th>

      <th>
        Default
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `title`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `""`
      </td>
    </tr>

    <tr>
      <td>
        `default_path`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `""`
      </td>
    </tr>

    <tr>
      <td>
        `default_name`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `""`
      </td>
    </tr>

    <tr>
      <td>
        `filters`
      </td>

      <td>
        `[]const FileFilter`
      </td>

      <td>
        <code>&.</code>
      </td>
    </tr>
  </tbody>
</table>

Returns an optional path string.

## Message dialog

<table>
  <thead>
    <tr>
      <th>
        Field
      </th>

      <th>
        Type
      </th>

      <th>
        Default
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `style`
      </td>

      <td>
        `MessageDialogStyle`
      </td>

      <td>
        `.info`
      </td>
    </tr>

    <tr>
      <td>
        `title`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `""`
      </td>
    </tr>

    <tr>
      <td>
        `message`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `""`
      </td>
    </tr>

    <tr>
      <td>
        `informative_text`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `""`
      </td>
    </tr>

    <tr>
      <td>
        `primary_button`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `"OK"`
      </td>
    </tr>

    <tr>
      <td>
        `secondary_button`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `""`
      </td>
    </tr>

    <tr>
      <td>
        `tertiary_button`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `""`
      </td>
    </tr>
  </tbody>
</table>

**MessageDialogStyle**: `info`, `warning`, `critical`

**MessageDialogResult**: `primary`, `secondary`, `tertiary`

## FileFilter

```zig
const filters = [_]native_sdk.FileFilter{
    .{ .name = "Images", .extensions = &.{ "png", "jpg", "gif" } },
    .{ .name = "All Files", .extensions = &.{ "*" } },
};
```

## From JavaScript

Dialogs require the [builtin bridge](/docs/bridge/builtin-commands) to be enabled with an explicit policy that grants the `dialog` permission. JSON field names use camelCase:

```javascript
const files = await window.zero.invoke("native-sdk.dialog.openFile", {
  title: "Select a file",
  defaultPath: "/home",
  allowMultiple: true,
  allowDirectories: false,
});

const path = await window.zero.invoke("native-sdk.dialog.saveFile", {
  title: "Save as",
  defaultName: "untitled.txt",
});

const result = await window.zero.invoke("native-sdk.dialog.showMessage", {
  style: "warning",
  title: "Confirm",
  message: "Delete this item?",
  informativeText: "This action cannot be undone.",
  primaryButton: "Delete",
  secondaryButton: "Cancel",
});
```

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Dynamic Images
url: "https://native-sdk.dev/docs/dynamic-images"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Load images at **runtime** — from disk or the network — decode them through the platform codec, and show them in views by id. `Cmd.imageLoad` is effects-as-data all the way down: your core names a model-owned `ImageId` and a source, the host does the I/O and the decode, and exactly one result Msg comes back — `loaded` with the decoded dimensions, or an error state. Views bind the id; nothing in `update` ever touches a file, a socket, or a codec.

Declare static images in the [manifest’s `assets.images`](/docs/app-zon) — read once at launch, registered before the first frame. This page is the **dynamic** half: cover art fetched from a CDN, a chart PNG a subprocess rendered, an avatar that arrives after login.

## Load, then show

The id is ordinary model data — pick it like an effect key. Issue the load, and write the id into the model **only when the result says `loaded`** (the store-on-success discipline): the view renders its fallback while the load is in flight and keeps it when the load failed, because an id of `0` — the no-image sentinel — draws nothing.

```ts
import { Cmd, asciiBytes } from "@native-sdk/core";

// The engine's outcome vocabulary — a named alias your core declares
// (the host matches the members by name, so the order is yours).
export type ImageState =
  | "loaded" | "rejected" | "not_found" | "io_failed" | "connect_failed"
  | "tls_failed" | "protocol_failed" | "timed_out" | "http_status"
  | "cancelled" | "too_large" | "unsupported" | "decode_failed" | "registry_full"
  | "alloc_failed";

export interface Model {
  readonly cover: number; // 0 until the load lands
  readonly coverState: ImageState;
}

export type Msg =
  | { readonly kind: "show" }
  | { readonly kind: "cover_done"; readonly id: number; readonly state: ImageState; readonly width: number; readonly height: number; readonly status: number };

export function initialModel(): Model {
  return { cover: 0, coverState: "rejected" };
}

export function update(model: Model, msg: Msg): Model | [Model, Cmd<Msg>] {
  switch (msg.kind) {
    case "show":
      return [model, Cmd.imageLoad(21, {
        path: asciiBytes("art/cover.png"),
        url: asciiBytes("https://cdn.example.com/art/cover.png"),
      }, { event: "cover_done" })];
    case "cover_done":
      if (msg.state === "loaded") return { ...model, cover: msg.id, coverState: msg.state };
      return { ...model, coverState: msg.state };
  }
}
```

```zig
const Model = struct {
    cover: canvas.ImageId = 0, // 0 until the load lands
};

// in update:
.show => fx.loadImage(.{
    .id = 21,
    .path = "art/cover.png",
    .url = "https://cdn.example.com/art/cover.png",
    .on_result = Effects.imageMsg(.cover_done),
}),
.cover_done => |result| if (result.outcome == .loaded) {
    model.cover = result.id;
},
```

The view binds the same model id. Markup's `image` element is the display-only leaf for exactly this — the binding is required (an unbound `<image>` is dead markup), the id is always `{binding}` model data (never a literal), and avatars carry the same attribute with an initials fallback:

```html
<image image="{cover}" width="120" height="80" label="Cover art" />
<avatar image="{avatar}" label="Octocat">OC</avatar>
```

The moment the result lands and `update` stores the id, every view referencing it repaints with the pixels — GPU caches re-upload off the changed content fingerprint, no invalidation calls. Zig views use `ui.image(.{ .image = model.cover, ... })`; both markup engines refuse a negative id binding at build time with a diagnostic, never a trap.

## The source cascade

The source resolves the way `Cmd.audioPlay` resolves — local first, network last, with a verified cache between:

1. **The local `path` is tried first.** A missing file falls through to `url` (when one is given); every other local failure is terminal — retrying a different source would mask the real problem.
2. **A verified cache entry loads without the network.** A URL source checks its cache path before fetching; `expectedBytes` (when you know the size, e.g. from a manifest) is the integrity gate — an entry whose size disagrees is discarded and re-fetched.
3. **The network fetch installs the cache behind it.** The bytes are fetched whole (no truncated delivery — a cut image can never decode), written beside the cache path, and atomically renamed into place only after the size verifies, so a partial download never occupies the cache name.

Prefer **omitting** `cachePath` for URL sources: when the app wiring configures a caches directory (`TsUiApp`'s `image_cache_dir`; the app runner points it at the platform caches directory automatically), the host derives the conventional content-addressed path — `<caches>/images/<sha256[..16]>.<ext>` — from the URL itself. Your update never builds filesystem paths, replay re-derives the same path by construction, and clearing the cache is deleting one directory the OS already treats as reclaimable.

<span id="one-result-honest-classes" />

## Load outcomes

Exactly one event arm dispatches per load — a five-field record matched by field name: `id`, `state`, `width`, `height`, `status`. `id` echoes the requested ImageId, so two loads in flight at once share one arm and still tell their results apart (that is what the example's `cover: msg.id` reads); `status` is the HTTP status for url loads that performed an exchange, and **0 when none occurred** — local paths and cache hits — so a `loaded` event with status 0 means the load did not perform an HTTP exchange; `loaded` means the pixels are registered and drawable; everything else names what actually happened:

- **Source classes** — `not_found` (missing local file, no url), `io_failed` (a local read failure), `connect_failed` / `tls_failed` / `protocol_failed` / `timed_out` (the fetch taxonomy, on the fetch machinery's own timeout), `http_status` (a non-2xx answer, with the status carried through — an error page is not an image, so the body is discarded).
- **Decode and registry classes** — `decode_failed`, `unsupported` (a host without a codec), `registry_full`, and `alloc_failed` (the host refused the memory the registration needed — resource exhaustion, not corrupt bytes: the same source may load once memory frees, so it is never reported as `decode_failed`). `too_large` normally means the encoded source exceeded 8 MiB; platform codecs fit decoded pixels to the app's declared target.
- **Discipline classes** — `rejected` (an invalid id, no source at all, or a duplicate live id: one load per id at a time, the spawn rule — a load in flight is never replaced implicitly) and `cancelled` (`Cmd.imageCancel(id)` ended the load).

`Cmd.imageCancel(id)` is the load's cancel — image loads are keyed by their numeric id, so the string-keyed `Cmd.cancel` never touches them. Cancel is **loud**, the spawn discipline: the one terminal still arrives, as the load's own event arm with state `cancelled`, and the id is free for a fresh load once it lands (a slow CDN fetch no longer pins its id against a retry). Aimed at an id with no live load it no-ops — whatever it targeted already delivered its terminal.

## Releasing images: the gallery eviction

A loaded image occupies one of the registry's **16 slots** until you release it, and `Cmd.imageUnregister(id)` is the release: the pixels are freed, views still referencing the id draw their fallback (avatar initials, nothing for `<image>`) on the next frame, and the slot is open for another load. For a gallery with more images than registry slots — a screen paging through covers loads the visible ids, and when the 17th image would answer `registry_full`, unregisters an off-screen evictee first (mint fresh ids for new content, effect-key style; never re-key different content onto a live id — the same rule the Zig tier's `fx.unregisterImage` documents).

Unlike a load, unregister is **synchronous registry surgery, not an effect**: no result Msg follows (registration by `imageLoad` has a terminal because I/O and decode can fail; releasing a slot cannot), and aimed at an id with no registration it no-ops — `imageCancel`'s idle rule. It frees only the **current** registration: a load in flight under the id is untouched, and its terminal still registers the pixels, re-occupying the id. To evict an id whose load is still running, `Cmd.imageCancel(id)` first, then unregister.

When many small visuals ship together, one slot can hold a texture atlas instead: `<image>` and `<avatar>` accept `source-x`, `source-y`, `source-width`, and `source-height` together, in decoded-image pixel coordinates. Zig views set the equivalent `ElementOptions.image_src` rectangle. Cropped widgets use nearest sampling so adjacent regions cannot bleed into the tile. Every atlas region references the same registered ImageId, so it consumes one registry slot; use the dimensions reported by the load result because decode-to-fit may change the atlas geometry.

<span id="limits-honestly" />

## Resource limits

The default is **16 slots** with a **1 MiB decoded-pixel target per slot**. That target is not a refusal: platform codecs decode photo-scale sources down, preserving aspect, until `width × height × 4` fits. A wide 1024×256 image already fits and stays that size; a 640×480 image registers at a smaller geometry, and the result's `width`/`height` report exactly what views draw. The encoded source has its own flat **8 MiB** bound; an over-bound source fails whole with `too_large`, never as truncated bytes.

Image-centric apps can raise the target in the app manifest, up to the hard 8 MiB ceiling:

```zig
.images = .{ .max_image_pixel_bytes = 8_388_608 },
```

The value is frozen at startup. Pixel blocks and decode scratch allocate lazily, so declaring a raise costs nothing until an image is used; each used registry slot is a whole-budget block, however, so filling all 16 slots at 8 MiB is a declared **128 MiB** high-water. The source bound stays 8 MiB regardless of this setting.

This gives three clear tiers: decode-to-fit is the default for feeds, galleries, avatars, and covers; the manifest raise serves wallpaper and image-forward apps that want more display-scale detail; pixel-exact editing, source-resolution zoom, and gigapixel tiling belong in an app-owned `gpu_surface` or [media-surface producer](/docs/media-producers), not the registry. The framework still bundles no codecs: bytes decode through CGImageSource on Apple platforms, gdk-pixbuf on GTK, WIC on Windows, BitmapFactory on Android, and the embed image service — a host without one answers `unsupported`.

For textures **produced** by your own renderer at video rates — a decoder, a camera, mpv — this is the wrong tool: that is the [media surface](/docs/media-producers)'s dynamic texture channel. `imageLoad` is for images that exist as encoded bytes somewhere and should become long-lived registered pixels.

## Recorded sessions replay byte-identical, offline

The loaded bytes **are** the effect result, and the session journal treats them that way. When a recorded session performs an image load, the encoded source bytes are written — at effect-result time — into a content-addressed blob store beside the journal (`blobs/<sha256[..16]>` in the session directory), and the journal record carries the hash and length. Loading the same bytes twice stores one blob: content addressing is deduplication — and the deduplicating probe verifies the existing blob's bytes before trusting its name, so a damaged blob is repaired in place on the next same-bytes recording rather than sealing a journal replay would refuse.

Replay reads the blob, re-runs decode and registration with the recorded bytes, and delivers the recorded result — **byte-identical and fully offline**: the original file, the network, and the cache are never consulted, and the fingerprint checkpoints verify the replayed session against the recording frame by frame. If the current manifest lowers the image budget after recording, the recorded result dimensions still replay verbatim while best-effort presentation re-decodes to the current budget; any resulting screenshot difference is a verification mismatch, not false journal damage. A journal whose `blobs/` directory is missing or damaged refuses loudly (the bytes are verified against their address) rather than replaying a different session. The blob record kind is journal format **v7** — older journals are refused at the preamble with a diagnostic requesting a new recording.

```sh
NATIVE_SDK_SESSION_RECORD=session/app.journal native run   # records blobs/ beside the journal
NATIVE_SDK_SESSION_REPLAY=session/app.journal native run   # replays offline, verifying fingerprints
```

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Embedded App
url: "https://native-sdk.dev/docs/embed"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

`EmbeddedApp` drives the runtime without the full platform event loop. Use it for embedding the Native SDK in an existing application, game engine, or custom render loop.

## Usage

```zig
var embedded = native_sdk.embed.EmbeddedApp.init(my_app.app(), my_platform);

try embedded.start();

// On app activation/deactivation:
try embedded.activate();
try embedded.deactivate();

// In your render loop:
try embedded.frame();

// On resize:
try embedded.resize(new_surface);

// On shutdown:
try embedded.stop();
```

## Methods

<table>
  <thead>
    <tr>
      <th>
        Method
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `init(app, platform)`
      </td>

      <td>
        Create an embedded app with a runtime
      </td>
    </tr>

    <tr>
      <td>
        `start()`
      </td>

      <td>
        Dispatch

        `app_start`

        event, loads the WebView source
      </td>
    </tr>

    <tr>
      <td>
        `activate()`
      </td>

      <td>
        Dispatch

        `app_activated`

        event
      </td>
    </tr>

    <tr>
      <td>
        `deactivate()`
      </td>

      <td>
        Dispatch

        `app_deactivated`

        event
      </td>
    </tr>

    <tr>
      <td>
        `resize(surface)`
      </td>

      <td>
        Dispatch

        `surface_resized`

        event
      </td>
    </tr>

    <tr>
      <td>
        `frame()`
      </td>

      <td>
        Dispatch

        `frame_requested`

        event
      </td>
    </tr>

    <tr>
      <td>
        `stop()`
      </td>

      <td>
        Dispatch

        `app_shutdown`

        event
      </td>
    </tr>
  </tbody>
</table>

## How it works

`EmbeddedApp` wraps a `Runtime` and an `App`. Each method dispatches a platform event via `runtime.dispatchPlatformEvent`, giving you full control over the event loop while still using Native SDK's runtime, bridge, and window management.

## Mobile examples

Mobile support is experimental as a whole, embedding included: everything here is real and verified — iOS on the simulator, Android on the emulator — but mobile APIs and tooling may still change, and desktop is the mature surface. See [Platform Support](/docs/platform-support#mobile).

The repository includes full mobile host examples:

- `examples/ios` - Xcode project with a Swift `UIViewController`, `WKWebView`, and `native_sdk.h` bridge.
- `examples/android` - Gradle/Kotlin project with JNI and CMake wiring for `libnative-sdk.a`.
- `examples/mobile-canvas` - canvas-scene mobile app built with `addMobileLib`: the user's `Model`/`Msg`/`update`/`view` app compiles into the embed static library, renders through the CPU reference renderer, and is presented by a minimal iOS or Android host shim with touch, keyboard, and IME forwarding.

The iOS and Android shell examples expect a local `libnative-sdk.a` built from the repository and copied into the path documented in each example README; the canvas example builds its own library through its `build.zig`.

## Mobile host contract

Mobile embedding uses the same runtime lifecycle through a thin native host:

<table>
  <thead>
    <tr>
      <th>
        Host event
      </th>

      <th>
        Native SDK call
      </th>

      <th>
        Current examples
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        View/controller creation
      </td>

      <td>
        `native_sdk_app_create`

        , configure packaged assets if needed, then

        `native_sdk_app_start`
      </td>

      <td>
        Swift

        `viewDidLoad`

        ; Android

        `onCreate`
      </td>
    </tr>

    <tr>
      <td>
        Packaged asset source
      </td>

      <td>
        `native_sdk_app_set_asset_root`

        and

        `native_sdk_app_set_asset_entry`

        before startup
      </td>

      <td>
        Generated package hosts point the runtime at the bundled asset root and manifest frontend entry
      </td>
    </tr>

    <tr>
      <td>
        App activation lifecycle
      </td>

      <td>
        `native_sdk_app_activate`

        ,

        `native_sdk_app_deactivate`
      </td>

      <td>
        Swift

        `SceneDelegate`

        active/resign-active callbacks; Android

        `onResume`

        /

        `onPause`
      </td>
    </tr>

    <tr>
      <td>
        Content size or surface changes
      </td>

      <td>
        `native_sdk_app_resize`

        , then

        `native_sdk_app_frame`
      </td>

      <td>
        Swift

        `viewDidLayoutSubviews`

        ; Android

        `surfaceChanged`

        with orientation/screen-size changes handled in the same activity
      </td>
    </tr>

    <tr>
      <td>
        Touch input
      </td>

      <td>
        `native_sdk_app_touch`

        , then

        `native_sdk_app_frame`
      </td>

      <td>
        Android forwards

        `MotionEvent`

        data; iOS keeps touch handling in UIKit/WebKit for now
      </td>
    </tr>

    <tr>
      <td>
        Keyboard and inset changes
      </td>

      <td>
        Host layout update, then

        `native_sdk_app_resize`

        and

        `native_sdk_app_frame`

        when the surface changes
      </td>

      <td>
        Swift keyboard frame notifications adjust the

        `WKWebView`

        constraint; Android uses

        `adjustResize`
      </td>
    </tr>

    <tr>
      <td>
        Native commands
      </td>

      <td>
        `native_sdk_app_command`
      </td>

      <td>
        Mobile chrome can dispatch stable command IDs such as

        `mobile.back`

        and

        `mobile.refresh`

        through the embedded runtime
      </td>
    </tr>

    <tr>
      <td>
        Shutdown
      </td>

      <td>
        `native_sdk_app_stop`

        , then

        `native_sdk_app_destroy`
      </td>

      <td>
        Swift controller deinit; Android

        `onDestroy`
      </td>
    </tr>
  </tbody>
</table>

The mobile examples keep navigation/header UI in UIKit or Android views and keep rich content in `WKWebView` or Android `WebView`. Native header buttons and the Android system Back action dispatch command IDs through `native_sdk_app_command`, so mobile chrome can use the same command/event model as desktop menus, shortcuts, and native controls. App activation, safe areas, orientation, keyboard avoidance, back gestures, and platform lifecycle ownership stay in the native host. Keyboard changes are handled by native layout first and forwarded to the Native SDK as resize/frame work when the content surface changes. Mobile `ShellView` metadata maps to generated native host config for the header labels, command buttons, and WebView workspace; dynamic runtime view mutation remains a desktop API for now.

## Declared platform chrome

Apps can declare platform chrome — a tab set and optionally one primary floating action — in shell metadata (`ShellConfig.chrome` in a Zig scene, `.shell.chrome` in app.zon):

```zig
const scene: native_sdk.ShellConfig = .{
    .windows = native_sdk.embed.mobile_shell_scene.windows,
    .chrome = .{
        .tabs = &.{
            .{ .id = "tabs.albums", .label = "Albums", .icon = "app:albums" },
            .{ .id = "tabs.songs", .label = "Songs", .icon = "music" },
        },
        .primary_action = .{ .id = "action.new", .label = "New", .icon = "plus" },
    },
};
```

On iOS the toolkit host projects the declaration as REAL native controls: an actual system tab bar and a real button, styled and made accessible by the OS — never imitated in canvas. Icons come from the icon vocabulary (built-in names, or `app:<name>` for icons the app registers) and are rasterized through the same vector core the canvas draws with into template images the system tints. The bar's overlap is folded into the safe-area insets the host already reports, so layout clears it through the existing `on_chrome` channel with no new code.

Selection state lives in the app's model. A tab tap dispatches the tab's declared command id through `native_sdk_app_command` — the same path as native header buttons — and the app maps it to a Msg in `on_command`; the model's `selected_tab_fn` derivation drives which item the bar shows active. The bar is a projection of the model, never the source of truth, so the whole loop is deterministic and replays from the Msg journal.

Desktop and Android hosts currently ignore this declaration. Desktop apps can compose tab controls in canvas. An app that also composes an in-canvas tab switcher can yield it while the chrome channel's `tabs_projected` flag is set, so exactly one tab affordance is ever on screen.

The same chrome channel carries the host-reported form factor (`WindowChrome.form_factor`): the iOS host reports its horizontal size class (`compact`/`regular`), so apps that switch shells can prefer host truth and keep a width derivation as the fallback for hosts that never report.

## Platform push/pop navigation

Apps whose model owns a page stack (a list opening into a detail) can project it as REAL platform navigation transitions. The seam is two options beside `selected_tab_fn`:

```zig
// Depth follows the VISIBLE page: 0 = the root, 1 = one push in. Tab
// switches are lateral, never depth — derive the current tab's stack.
pub fn navigationDepth(model: *const Model) usize {
    return if (model.tab == .albums and model.open_album != null) 1 else 0;
}

// In mobileOptions():
.navigation_depth_fn = navigationDepth,
.navigation_back_command = "nav.back",
```

The MODEL owns navigation state; the platform contributes the transition, never the state. The iOS host polls the depth once per tick: when it grows by one the host presents the system push transition, and when it shrinks by one the system pop. Both sides of a transition are exact frames of the two model states — the outgoing page is the previous frame (retained from the damage path with zero extra renders), and the incoming state is rendered through the ordinary damage delivery before the animation starts, so the transition is pixel-correct for its whole run and the live canvas swaps back onto the top page the moment it lands. A depth change that arrives together with a selected-tab change is a lateral tab switch and reconciles with no transition, and so does any multi-level jump — standard platform behavior, no theater. Rapid changes coalesce the same way: the stack converges to the model's current depth rather than replaying every intermediate hop.

The interactive edge-swipe-back is the system's own recognizer with its real physics, armed exactly while the depth is above zero and a `navigation_back_command` exists. Completing the gesture dispatches that command through the embed command path exactly once — the same journal entry the app's own back button produces when both map to the same Msg in `on_command` — and cancelling it dispatches nothing, leaving the model untouched. During the gesture the revealed page is the snapshot retained at push time (one full-resolution frame per depth level, bounded), which can be stale if the shallow page changed underneath; a fresh render replaces it the moment the gesture completes. If the app ignores the dispatched back command, the next poll sees the un-popped depth and pushes the page back — model truth wins, the tab-bar snap-back rule applied to navigation.

Because the depth is a pure derivation and the gesture enters through the ordinary command path, the whole loop stays deterministic: a journal replayed without a host produces the identical model, and two hosts driven by the same journal project the identical stack. Desktop and Android hosts currently ignore this declaration. Desktop apps can compose navigation in canvas; Android system-back projection is not available.

## Testing with EmbeddedApp

```zig
var null_platform = native_sdk.NullPlatform.init(.{});
var state: u8 = 0;
var embedded = native_sdk.embed.EmbeddedApp.init(.{
    .context = &state,
    .name = "embedded",
    .source = native_sdk.WebViewSource.html("<p>Embedded</p>"),
}, null_platform.platform());

try embedded.start();
// null_platform.loaded_source now contains the loaded HTML
```

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Extensions
url: "https://native-sdk.dev/docs/extensions"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The `ModuleRegistry` provides a hook-based extension system for adding modular capabilities to the runtime.

## Module structure

Each module has an info block, a context pointer, and optional lifecycle hooks:

```zig
const MyModule = struct {
    data: u32 = 0,

    fn start(context: *anyopaque, runtime: native_sdk.extensions.RuntimeContext) anyerror!void {
        _ = runtime;
        const self: *@This() = @ptrCast(@alignCast(context));
        self.data = 42;
    }

    fn command(context: *anyopaque, runtime: native_sdk.extensions.RuntimeContext, cmd: native_sdk.extensions.Command) anyerror!void {
        _ = runtime;
        const self: *@This() = @ptrCast(@alignCast(context));
        if (std.mem.eql(u8, cmd.name, "reset")) self.data = 0;
    }
};
```

## Registering modules

```zig
var my_module = MyModule{};
const caps = [_]native_sdk.extensions.Capability{.{ .kind = .native_module }};
const modules = [_]native_sdk.extensions.Module{.{
    .info = .{ .id = 1, .name = "my-module", .capabilities = &caps },
    .context = &my_module,
    .hooks = .{ .start_fn = MyModule.start, .command_fn = MyModule.command },
}};
const registry = native_sdk.extensions.ModuleRegistry{ .modules = &modules };

var runtime = native_sdk.Runtime.init(.{
    .platform = my_platform,
    .extensions = registry,
});
```

## Module fields

<table>
  <thead>
    <tr>
      <th>
        Field
      </th>

      <th>
        Type
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `info.id`
      </td>

      <td>
        `u64`
      </td>

      <td>
        Unique numeric identifier (duplicates rejected at validation)
      </td>
    </tr>

    <tr>
      <td>
        `info.name`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        Human-readable module name
      </td>
    </tr>

    <tr>
      <td>
        `info.capabilities`
      </td>

      <td>
        `[]const Capability`
      </td>

      <td>
        Capabilities this module provides
      </td>
    </tr>

    <tr>
      <td>
        `context`
      </td>

      <td>
        `*anyopaque`
      </td>

      <td>
        Opaque pointer to module state
      </td>
    </tr>

    <tr>
      <td>
        `hooks.start_fn`
      </td>

      <td>
        optional
      </td>

      <td>
        Called when the runtime starts
      </td>
    </tr>

    <tr>
      <td>
        `hooks.stop_fn`
      </td>

      <td>
        optional
      </td>

      <td>
        Called when the runtime stops (reverse registration order)
      </td>
    </tr>

    <tr>
      <td>
        `hooks.command_fn`
      </td>

      <td>
        optional
      </td>

      <td>
        Called when a command is dispatched to modules
      </td>
    </tr>
  </tbody>
</table>

## Registry methods

<table>
  <thead>
    <tr>
      <th>
        Method
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `validate()`
      </td>

      <td>
        Check for duplicate module IDs
      </td>
    </tr>

    <tr>
      <td>
        `startAll(runtime)`
      </td>

      <td>
        Call

        `start_fn`

        on all modules
      </td>
    </tr>

    <tr>
      <td>
        `stopAll(runtime)`
      </td>

      <td>
        Call

        `stop_fn`

        on all modules (reverse order)
      </td>
    </tr>

    <tr>
      <td>
        `dispatchCommand(runtime, command)`
      </td>

      <td>
        Call

        `command_fn`

        on all modules
      </td>
    </tr>

    <tr>
      <td>
        `hasCapability(kind)`
      </td>

      <td>
        Check if any module provides a capability
      </td>
    </tr>
  </tbody>
</table>

## Capability kinds

<table>
  <thead>
    <tr>
      <th>
        Kind
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `native_module`
      </td>

      <td>
        A native Zig module
      </td>
    </tr>

    <tr>
      <td>
        `webview`
      </td>

      <td>
        WebView rendering
      </td>
    </tr>

    <tr>
      <td>
        `js_bridge`
      </td>

      <td>
        JavaScript bridge
      </td>
    </tr>

    <tr>
      <td>
        `filesystem`
      </td>

      <td>
        File system access
      </td>
    </tr>

    <tr>
      <td>
        `network`
      </td>

      <td>
        Network access
      </td>
    </tr>

    <tr>
      <td>
        `clipboard`
      </td>

      <td>
        Clipboard access
      </td>
    </tr>

    <tr>
      <td>
        `custom`
      </td>

      <td>
        Custom capability (with a

        `name`

        field)
      </td>
    </tr>
  </tbody>
</table>

These correspond to capability kinds used by the [app manifest](/docs/app-zon). The runtime can query whether any module provides a given capability using `registry.hasCapability(.filesystem)`.

## Native JS engine (experimental)

The `js` module provides an abstraction layer for calling into a native JavaScript engine from Zig. This is separate from the WebView bridge and is intended for future native module integrations.

<table>
  <thead>
    <tr>
      <th>
        Type
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `Value`
      </td>

      <td>
        Tagged union:

        `null`

        ,

        `boolean`

        ,

        `number`

        ,

        `string`
      </td>
    </tr>

    <tr>
      <td>
        `Call`
      </td>

      <td>
        A function call:

        `module`

        ,

        `function`

        ,

        `args`
      </td>
    </tr>

    <tr>
      <td>
        `Bridge`
      </td>

      <td>
        Validates and dispatches calls via

        `RuntimeHooks`
      </td>
    </tr>

    <tr>
      <td>
        `NullEngine`
      </td>

      <td>
        Stub that returns

        `EngineUnavailable`
      </td>
    </tr>
  </tbody>
</table>

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Files and streaming
url: "https://native-sdk.dev/docs/files"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use raw file effects for user-owned files: imports, exports, recordings, images, CSV, and other blobs. App state belongs in [Model Persistence](/docs/persistence), the [Record Store](/docs/record-store), or [Relational SQLite](/docs/sqlite); secrets belong in credentials.

Every operation is still an effect. `update` returns a `Cmd`, the host performs I/O after the model commits, and results return as later `Msg` values. Session recording journals those results; streamed chunks live in the content-addressed `blobs/` directory beside the journal so replay never opens the original file.

The blob directory is deduplicated but currently has no quota or automatic GC; see [Automation: Session recording and blob growth](/docs/automation#session-recording-and-blob-growth).

## Small whole-file operations

`Cmd.readFile` and `Cmd.writeFile` remain convenient for payloads up to 1 MiB. An over-bound write is rejected. An over-bound read returns `truncated` rather than passing cut bytes as a successful file. `Cmd.appendFile` appends one payload up to 1 MiB, `Cmd.statFile` reports `{ exists, size, mtimeMs }` without reading the content, and `Cmd.deleteFile` removes one file.

```ts
return [model, Cmd.statFile(path, {
  ok: "file_stat",
  err: "file_failed",
})];

return [model, Cmd.deleteFile(path, {
  ok: "file_deleted",
  err: "file_failed",
})];
```

Deletion is file-only and never recursive: it reports `not_found` when the path is absent and `io_failed` for a directory or another OS refusal. If the final path component is a symlink, deletion removes the link rather than its target. File outcomes are closed and machine-readable: `ok`, `not_found`, `io_failed`, `truncated`, `rejected`, `cancelled`, `sink_missing`, `out_of_order`, and `disk_full`.

## Streaming reads

`Cmd.readFileStream` delivers zero or more 256-KiB chunks, followed by exactly one `done` message carrying the total byte count, or one `err` message.

Read-stream keys follow ordinary file semantics: reissuing a live key silently replaces the old read, and `Cmd.cancel(key)` silently drops it. Stale chunks from the retired generation cannot reach the replacement route.

```ts
return [model, Cmd.readFileStream(path, {
  key: "import",
  chunk: "import_chunk",
  done: "import_done",
  err: "import_failed",
})];
```

Streaming has its own four-slot budget and no total-size ceiling. Each individual chunk remains bounded, so one large import cannot consume the sixteen general spawn/fetch/whole-file slots.

## Atomic streaming writes

Open a sink, send one acknowledged chunk at a time, then close it. Close syncs the temporary file and atomically replaces the destination; before a successful close, the old destination stays visible and teardown removes the temporary file.

```ts
Cmd.writeFileStream("export", path, { ok: "sink_open", err: "export_failed" });
Cmd.writeFileChunk("export", bytes, { ok: "chunk_written", err: "export_failed" });
Cmd.writeFileClose("export", { ok: "export_done", err: "export_failed" });
```

A duplicate live sink rejects. A chunk or close issued before the previous acknowledgment returns `out_of_order`; a chunk or close with no open sink returns `sink_missing`. Chunks may be at most 1 MiB.

Write sinks report cancellation: `Cmd.cancel(key)` ends the sink through `err: cancelled` and removes its temporary file. A half-written export is never silently replaced or installed.

## Filesystem permission

Raw paths inside this app's resolved `data`, `config`, `cache`, `state`, `logs`, and `temp` directories need no permission. Any path outside those roots requires:

```zig
.permissions = .{ "filesystem" },
```

The runtime is authoritative. Before checking, it resolves the target when it exists, or resolves the deepest existing parent and normalizes the missing suffix. Existing symlinks are followed for authorization, so a symlink inside an app directory that points outside is external and requires the permission. Deletion still unlinks the final symlink itself after that check. `..` cannot escape an allowed root.

`native check` also reports NS1074 for certainly-external literal paths, but dynamic paths are decided only by the runtime. File pickers therefore require the `filesystem` permission because the selected user file is normally outside app-owned directories.

Directory enumeration, file watching, chmod/permissions, and per-path grant prompts are not part of this API.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Fonts
url: "https://native-sdk.dev/docs/fonts"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The SDK bundles two faces — Geist Regular and Geist Mono — and every built-in component renders through them by default. Bundled coverage is Latin-script: a codepoint outside it (CJK, emoji, most symbols) draws as the face's notdef box — tofu. Registering a font is how an app renders a script the bundled faces do not cover: register a face that covers the characters, point the typography tokens at it, and both renderers ink the registered outlines. Chinese and Japanese text render this way today.

## Registering a face

Declare fonts on `UiApp.Options.fonts` and the app registers them on the installing frame, before the first layout measures anything:

```zig
const MyApp = native_sdk.UiApp(Model, Msg);

/// App-chosen id, at or above canvas.min_registered_font_id (64).
/// Store the id in your theme; ids below 64 are reserved for the
/// built-in faces.
pub const cjk_font_id: canvas.FontId = canvas.min_registered_font_id;

const app_fonts = [_]MyApp.FontRegistration{.{
    .id = cjk_font_id,
    .name = "NotoSansSC-Regular.ttf", // teaching errors name this
    .ttf = @embedFile("fonts/NotoSansSC-Regular.ttf"),
}};

fn tokensFromModel(model: *const Model) canvas.DesignTokens {
    var tokens = canvas.DesignTokens.theme(.{ .color_scheme = model.scheme });
    // Text resolves its face through the typography tokens: pointing
    // font_id at the registered id covers every ordinary run. Mono
    // spans keep the bundled mono face unless you also point
    // typography.mono_font_id at a face that covers your script.
    tokens.typography.font_id = cjk_font_id;
    return tokens;
}

pub fn options() MyApp.Options {
    return .{
        .name = "hello-cjk",
        .scene = shell_scene,
        .canvas_label = "canvas",
        .update = update,
        .view = view, // e.g. ui.text(.{}, model.greeting) with greeting = "你好，世界"
        .tokens_fn = tokensFromModel,
        .fonts = &app_fonts,
    };
}
```

The face must be a TrueType (`glyf`) file. The Google Fonts TrueType builds of the Noto CJK faces (Noto Sans SC/TC/JP/KR, OFL-licensed) register as-is; commit the file with its license next to your sources, like any other asset. Underneath `Options.fonts` sits the runtime seam the option calls, one face at a time — embedders and custom lifecycles use it directly:

```zig
try runtime.registerCanvasFont(cjk_font_id, ttf_bytes);
```

Register at startup so the first layout already measures with the face. Late registration triggers a rebuild: `UiApp` surfaces re-measure and rebuild automatically when a face joins the registry, and callers driving the runtime directly re-emit their display lists after registering.

## Validation is loud and registration-time only

Registration validates the font file, identity, and declared glyph bounds. `Options.fonts` logs each error with the font name before propagating it. Point-matched composite placement is checked when the affected glyph first renders; unsupported placement draws a block fallback.

- `error.InvalidFontId` — id 0 is the "inherit run font" sentinel and can never hold a face.
- `error.ReservedFontId` — ids below `canvas.min_registered_font_id` (64) belong to the built-in faces.
- `error.FontTooLarge` — exceeds the 24 MiB per-font limit (`canvas_limits.max_registered_canvas_font_bytes`).
- `error.FontParseFailed` — not a parseable TrueType `glyf` face; `canvas.font_ttf.parseFailureReason(ttf)` names what is wrong (CFF/OpenType `.otf` files fail here — use the TrueType build).
- `error.FontExceedsGlyphBudgets` — the `maxp` table exceeds 1024 points or 128 contours per glyph. Composites have the same flattened limits, with at most 4 levels and 8 components. `canvas.font_ttf.declaredGlyphMaxima(ttf)` exposes the declared values.
- `error.FontIdInUse` — registration is permanent for the runtime's lifetime and there is deliberately no unregister: glyph caches key by (font id, glyph id) with no content fingerprint, so replacing an id's bytes would serve stale glyphs. Re-registering an id fails; give each face its own id.
- `error.FontRegistryFull` — at most 8 registered faces per runtime (`canvas_limits.max_registered_canvas_fonts`).
- `error.FontHostRegistrationUnsupported` — the host measures text separately but cannot register the requested face.

## Ownership and lifecycle

Registration copies the file into an exact-size heap allocation owned by the runtime, so the caller's buffer (an `@embedFile` slice, a transient read) is free the moment the call returns. A runtime with no registered fonts carries zero font bytes. The copy lives until `Runtime.deinit` — registration is permanent, so ownership is one allocation and one free. Apps that live for the process never think about this; embedders that create and destroy runtimes call `Runtime.deinit` (the SDK's own embed hosts already do). `Runtime.deinit` returns the host-side registration too: on macOS, each registered id's host font state (the CoreText descriptor and its measurement caches) is dropped at deinit, so runtime cycles do not accumulate host entries. Host font state is one face per id per process — concurrent runtimes in one process that share an id resolve the most recent registration; that is a deliberate current constraint, so give concurrently live runtimes disjoint ids.

## How text finds the face

There is no per-widget font attribute. Text resolves its face through the typography tokens — `typography.font_id` for every ordinary run, `typography.mono_font_id` for mono-flagged runs, and optional `typography.button_font_id` for control labels — so a registered face is a theme decision, and the id rides everywhere a `canvas.FontId` rides: token overrides, both renderers, glyph atlas keys, render fingerprints.

Layout and ink stay in lockstep on the desktop paths. On macOS, the raw bytes reach the host at registration time, so CoreText measurement and packet text drawing resolve the exact registered face even when the family is not installed system-wide. On Windows and Linux, layout charges the parsed face's own `cmap`/`hmtx` advances, and the deterministic reference renderer inks the same outlines — CJK advances measure as the face declares them, not as the Latin estimator would guess. Mobile hosts do not yet have a registered-font measurement seam, so this guarantee does not extend there — see the platform notes below.

Codepoints the registered face does not cover keep the same per-glyph fallback as the built-ins: the face's own notdef box. The engine never silently cascades into another family — the reference renderer, the glyph atlas, and packet text through the SDK's rasterizer show missing coverage as visible notdef, never an approximation. Text the macOS host draws follows platform shaping, including system-font fallback, so uncovered codepoints there may render from a substituted family instead of notdef.

The interaction with the [tofu guard](/docs/native-ui#tooling): the static markup check validates literal text against the *bundled* face's coverage — it runs without your app and cannot know what you register. Text in a script outside bundled coverage reaches the view through model bindings (`{greeting}` — the guard deliberately skips binding spans, and real CJK content is almost always data) or through Zig-built views, where the same lesson is a Debug-build diagnostic rather than an error.

## Platform truth

The registration pipeline — TTF parsing, glyph outlines, rasterization — is the SDK's own code, identical on every platform. macOS additionally registers the face with CoreText so measurement and Metal packet text agree with it; Windows registers the same bytes with DirectWrite for its retained packet path. Linux and Windows software fallback ink the registered outlines directly. The suite pins reference-renderer pixel behavior for a registered face, and a CI receipt registers a CJK face and proves Chinese text renders as real glyphs on a native Windows runner. Mobile hosts render the same reference-renderer pixels, but no mobile test exercises registered fonts yet and the mobile hosts' text measurement has no registered-font seam — treat registered fonts on iOS and Android as unverified today. See [Platform Support](/docs/platform-support#support-matrix) for the per-platform matrix.

## The TypeScript tier

Font registration is Zig-tier today: TypeScript cores have no fonts surface yet.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Web Content
url: "https://native-sdk.dev/docs/frontend"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use a WebView to embed a browser, website preview, or documentation viewer in a native app. The [browser example](https://github.com/vercel-labs/native/tree/main/examples/browser) is a complete working browser; the [web engines](/docs/web-engines) page covers choosing between the platform WebView and bundled Chromium.

The same machinery also carries an entire existing web frontend — a React, Vue, Svelte, or Next.js codebase running in a WebView inside the native shell. Use this path to bring an existing frontend into a desktop app. The default scaffold uses [Native markup](/docs/native-ui). Scaffold one with `native init my_app --frontend <next|vite|react|svelte|vue>`.

For frontends with a build step, the Native SDK provides helpers to switch between a dev server during development and bundled assets in production.

## Dynamic source function

Use `source_fn` on your `App` so development uses a localhost server and production uses bundled assets:

```zig
fn source(context: *anyopaque) anyerror!native_sdk.WebViewSource {
    const self: *App = @ptrCast(@alignCast(context));
    return native_sdk.frontend.sourceFromEnv(self.env_map, .{
        .dist = "dist",
        .entry = "index.html",
    });
}
```

`sourceFromEnv` checks `NATIVE_SDK_FRONTEND_URL`. If set, it returns a URL source; otherwise it returns an assets source from `config.dist`.

## frontend.Config

<table>
  <thead>
    <tr>
      <th>
        Field
      </th>

      <th>
        Default
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `dist`
      </td>

      <td>
        `"dist"`
      </td>

      <td>
        Path to the built frontend output
      </td>
    </tr>

    <tr>
      <td>
        `entry`
      </td>

      <td>
        `"index.html"`
      </td>

      <td>
        HTML entry point within dist
      </td>
    </tr>

    <tr>
      <td>
        `origin`
      </td>

      <td>
        `"zero://app"`
      </td>

      <td>
        Origin for asset URLs
      </td>
    </tr>

    <tr>
      <td>
        `spa_fallback`
      </td>

      <td>
        `true`
      </td>

      <td>
        Serve entry for unknown routes
      </td>
    </tr>

    <tr>
      <td>
        `dev_url_env`
      </td>

      <td>
        `"NATIVE_SDK_FRONTEND_URL"`
      </td>

      <td>
        Environment variable checked by

        `sourceFromEnv`
      </td>
    </tr>
  </tbody>
</table>

## Configure in app.zon

```zig
.frontend = .{
    .dist = "dist",
    .entry = "index.html",
    .spa_fallback = true,
    .dev = .{
        .url = "http://127.0.0.1:5173/",
        .command = .{ "npm", "run", "dev", "--", "--host", "127.0.0.1" },
        .ready_path = "/",
        .timeout_ms = 30000,
    },
}
```

## Dev server

Use `native dev` to let the Native SDK manage the frontend server lifecycle:

```bash
native dev --binary zig-out/bin/MyApp
native dev --binary zig-out/bin/MyApp --url http://127.0.0.1:3000/ --command "npm run dev"
```

The command starts the frontend process, waits for the port to accept connections, launches the native shell with `NATIVE_SDK_FRONTEND_URL`, and terminates the frontend when the shell exits. See [Dev Server](/docs/cli/dev) for all flags.

## Framework recipes

**Vite**: `.url = "http://127.0.0.1:5173/"`, command `npm run dev -- --host 127.0.0.1`.

**Next.js**: `.url = "http://127.0.0.1:3000/"`, command `npm run dev -- --hostname 127.0.0.1`.

**Static preview**: point `.dist` at the build output and use any local server command.

## Examples

The repository includes complete frontend examples:

- `examples/next` - Next.js app with `frontend/out` production assets.
- `examples/react` - React app built with Vite.
- `examples/svelte` - Svelte app built with Vite.
- `examples/vue` - Vue app built with Vite.

Each example can be run from its directory with `zig build run`, or with `zig build dev` for the managed frontend dev server flow.

## Production source

For packaged builds that always use local assets:

```zig
return native_sdk.frontend.productionSource(.{ .dist = "dist", .entry = "index.html" });
```

## NATIVE\_SDK\_FRONTEND\_ASSETS

`NATIVE_SDK_FRONTEND_ASSETS` is an app-defined convention (not read by the `frontend` module). Examples use it to signal that pre-built assets should be loaded via `productionSource` instead of the default dev/prod branching.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Introduction
url: "https://native-sdk.dev/docs/introduction"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Native SDK is an open source toolkit for building native desktop applications with TypeScript and Native markup. It compiles app logic and views into a native executable and renders the interface in OS windows. The toolkit is implemented in Zig; apps can also use a Zig core.

Native-rendered apps do not require a browser or JavaScript engine at runtime. Apps that need web content can include [WebView surfaces](/docs/frontend) alongside native views.

## Create an app

Start with the [Quick Start](/docs/quick-start) to install the CLI, generate a counter app, and run it. The default project uses TypeScript for app logic and `.native` files for the interface. The CLI manages the build graph; use [Config](/docs/app-zon) to set app identity, windows, permissions, and security policy.

<span id="simple-authoring" />

## App structure

The main files in a generated app are:

- `src/core.ts`: the model, typed messages, update function, and effect declarations.
- `src/app.native`: layout, model bindings, and message dispatch.
- `app.json`: the app manifest. Existing `app.zon` manifests remain supported.

App cores use a checked [TypeScript subset](/docs/typescript). Filesystem access, network requests, and other external work run through effects and [TypeScript services](/docs/typescript/services). Create a project with `--template zig-core` to write the core in Zig instead.

<span id="predictable-state" />

## State and development

Events dispatch messages to `update`, which produces the next model and any effects. The view derives its content from the model; markup reads state and dispatches messages without mutating it. See [App Model](/docs/app-model) for the runtime loop and [State & Data Flow](/docs/state) for data binding patterns.

`native dev` rebuilds markup while preserving app state. Core edits rebuild and restart the app. Use `native dev --core` to exercise TypeScript logic under Node.js without opening a window.

<span id="beautiful-by-default" />

<span id="customizable-by-design" />

## Components and themes

The [component catalog](/docs/components) includes buttons, text inputs, lists, tables, dialogs, and charts. Components use named design tokens for colors, spacing, typography, and other styling. Configure tokens and switch themes through the [theming API](/docs/theming).

<span id="native-from-the-start" />

## Platform support

Native SDK has desktop hosts for macOS, Linux, and Windows. Rendering backends and OS capabilities vary by host. Mobile support is experimental. Check [Platform Support](/docs/platform-support) for the status of each capability before choosing a target.

<span id="ai-is-part-of-the-workflow" />

## Testing and automation

[Tests](/docs/testing) can exercise app logic and runtime integration without a window server. Apps can also enable an [automation server](/docs/automation) to expose accessibility snapshots, accept input commands, and capture screenshots. The CLI includes [agent skills](/docs/skills) for app authoring and inspection.

<span id="where-to-go-next" />

## Next steps

- [Quick Start](/docs/quick-start): install the CLI and run your first app.
- [CLI](/docs/cli): commands and options.
- [Native UI](/docs/native-ui): markup, layout, and bindings.
- [Packaging](/docs/packaging): create distributable packages.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Keyboard Shortcuts
url: "https://native-sdk.dev/docs/keyboard-shortcuts"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The Native SDK can register app-level keyboard shortcuts and deliver them to both Zig and JavaScript. Shortcuts are matched by key plus modifiers and are scoped to the native window that receives the key event.

## Manifest

Declare shortcuts in the app manifest. This example uses ZON:

```zig
.shortcuts = .{
    .{ .id = "command.palette", .key = "p", .modifiers = .{ "primary", "shift" } },
    .{ .id = "reload", .key = "r", .modifiers = .{ "primary" } },
},
```

`primary` maps to Command on macOS and Control on Windows/Linux. Use `command`, `control`, `option` or `alt`, and `shift` when a platform-specific modifier is required.

An app can define up to 64 shortcuts. Shortcut ids can be up to 64 bytes and keys can be up to 32 bytes.

Chromium builds are currently macOS-only. Use the system WebView backend on Linux when an app needs native shortcut events.

## Runtime

Generated runners register shortcuts from `app.json` or `app.zon` automatically. Use `RunOptions.shortcuts` only when an app needs to override the manifest with a list built in Zig.

Apps that construct a runtime directly can pass shortcuts in `Runtime.init`:

```zig
const shortcuts = [_]native_sdk.Shortcut{
    .{ .id = "command.palette", .key = "p", .modifiers = .{ .primary = true, .shift = true } },
};

var runtime = native_sdk.Runtime.init(.{
    .platform = platform,
    .shortcuts = &shortcuts,
});
```

## Events

Zig apps receive `Event.shortcut`:

```zig
fn event(context: *anyopaque, runtime: *Runtime, ev: Event) anyerror!void {
    _ = context;
    _ = runtime;
    switch (ev) {
        .shortcut => |shortcut| {
            if (std.mem.eql(u8, shortcut.id, "command.palette")) {
                // Open palette
            }
        },
        else => {},
    }
}
```

The JavaScript bridge emits a `shortcut` event on `window.zero`:

```ts
window.zero.on("shortcut", (event) => {
  if (event.id === "command.palette") {
    openCommandPalette();
  }
});
```

The event detail includes `id`, `command`, `key`, `windowId`, and `modifiers`. `command` is an alias for `id`.

## Keys

Portable single-character keys are letters, digits, and the unshifted punctuation keys `=`, `-`, `,`, `.`, `/`, `;`, `'`, `[`, `]`, `\`, and `` ` ``. Named keys are `escape`, `enter`, `tab`, `space`, `backspace`, `arrowleft`, `arrowright`, `arrowup`, `arrowdown`, `delete`, `home`, `end`, `pageup`, `pagedown`, `insert`, and `f1` through `f12`.

Single-character keys and text-entry keys (`enter`, `tab`, `space`, and `backspace`) require at least one modifier so shortcuts do not intercept normal typing by default.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Media Producers
url: "https://native-sdk.dev/docs/media-producers"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Drive a `media-surface` from your own renderer: a video decoder, a camera pipeline, a game engine, or an external player like **mpv**. This is a Zig-tier toolkit extension — the producer API lives on the runtime, below `Cmd`/`Sub`, and this page is its recipe. For built-in playback and transport controls, see [video](/docs/components/video).

## The shape

A [`media-surface`](/docs/components/media-surface) widget binds a model-owned `u64` surface id. Your producer acquires a handle for the same id and pushes frames; the compositor does the rest — layout, clipping, z-order, damage tracking, and pacing against the presented-frame clock.

```zig
// 1. The model owns the surface id — any nonzero id below the reserved
// media-surface texture bit (if you hash into a u64, clear bit 63).
const Model = struct {
    player_surface: u64 = 1,
};

// 2. The view binds it (markup: <media-surface surface="{player_surface}">).
ui.mediaSurface(.{ .image = model.player_surface, .grow = 1, .semantics = .{ .label = "Player" } })

// 3. Your producer claims the channel once (loop thread)...
const producer = try runtime.acquireMediaSurfaceProducer(model.player_surface);

// 4. ...and pushes tightly-packed straight-alpha RGBA8 frames from ANY
// thread — your decoder's, your engine's render loop, an mpv
// render-callback. The pixels are copied before pushFrame returns.
try producer.pushFrame(width, height, rgba8);

// 5. Release when the producer shuts down.
producer.release();
```

## The contract

- **Latest-wins.** Pushes never queue: an unpresented staged frame is replaced by the next one, so a producer running faster than the display can never build a backlog. Push at your source's native rate and let the compositor sample.
- **Paced by the presented-frame clock, woken by your pushes.** Staged frames are adopted when the compositor's frame ticks (the 60 Hz host timer, or a prompt requested frame) — and a push that stages new bytes while the app is idle requests one coalesced frame itself, so a 24 fps video in an app nobody touches never stalls and a producer that starts late is adopted promptly. There is no wake channel to manage; pushing is waking.
- **Damage-tracked.** Matching frame bytes are detected by hashing, avoiding a new copy and damage request.
- **Any-thread pushes, teardown-safe.** `pushFrame` and `release` are callable from any thread and touch only process-lived channel memory. A producer that outlives its view — or the whole runtime — pushes into inert memory; it can never use-after-free. Pushes through a released handle report `error.MediaSurfaceReleased` loudly.
- **RGBA8 format.** Submit tightly packed straight-alpha RGBA8. Other pixel formats and GPU handles are not currently accepted.
- **Bounded and loud.** Frames are bounded per channel (1080p RGBA8 fits; `error.FrameTooLarge` past it) and channels are bounded process-wide (`error.MediaSurfaceChannelsExhausted`); one live producer per surface id (`error.MediaSurfaceInUse`).
- **GPU hosts composite; software hosts show the placeholder.** Textures reach the screen through the GPU packet pipeline. A host on the software pixel fallback renders through the deterministic reference path, which shows the placeholder by policy in this release.

<span id="determinism-honestly" />

## Recording and replay

Texture *contents* are presentation chrome. The deterministic reference renderer — goldens, reference screenshots, session-replay pixel marks — renders a placeholder derived from the surface id, never your frames, and session fingerprints hash the semantic tree, which carries the surface's existence, geometry, and label but no pixels. A session recorded while your producer runs replays fingerprint-identical with the producer absent. Test your *app* with the toolkit's replay machinery; test your *frames* in your own pipeline.

## The mpv shape (sketch)

mpv's `render` API hands you frames on its own thread; the media surface is built for exactly that hand-off:

```zig
// mpv render callback (mpv's thread): render into your RGBA buffer,
// then push. Latest-wins absorbs any rate mismatch with the display.
fn onMpvFrame(player: *Mpv, producer: media.MediaSurfaceProducer) void {
    const frame = player.renderFrameRgba8(); // your mpv glue
    producer.pushFrame(frame.width, frame.height, frame.pixels) catch |err| switch (err) {
        error.MediaSurfaceReleased => player.stop(), // the app tore the surface down
        else => {},
    };
}
```

This is an integration sketch; a complete mpv example is not included.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Menus
url: "https://native-sdk.dev/docs/menus"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The Native SDK can configure native app menus from the app manifest or typed Zig data. Menu items dispatch through the same command path as shortcuts, native controls, tray actions, and bridge commands.

```zig
.menus = .{
    .{
        .title = "View",
        .items = .{
            .{ .label = "Refresh", .command = "app.refresh", .key = "r", .modifiers = .{ "primary" } },
            .{ .separator = true },
        },
    },
},
```

Generated zero-config runners load manifest menus automatically for both TypeScript cores and Zig cores. Ejected runners use the same fallback. Pass a non-null `menus` slice to `runWithOptions` when lower-level Zig wiring needs to override the manifest at runtime (an explicit empty slice disables manifest menus):

```zig
const view_items = [_]native_sdk.MenuItem{
    .{
        .label = "Refresh",
        .command = "app.refresh",
        .key = "r",
        .modifiers = .{ .primary = true },
    },
};

const menus = [_]native_sdk.Menu{
    .{ .title = "View", .items = &view_items },
};

try runner.runWithOptions(app.app(), .{
    .app_name = "native-shell",
    .bundle_id = "dev.native_sdk.native_shell",
    .menus = &menus,
}, init);
```

When a user selects a command-backed menu item, the runtime emits `Event.command` with `source = .menu`, the active native `window_id`, and the item command name.

The macOS, Linux, and Windows system-WebView backends apply configured menus as native app/window menus. Backends that do not implement native menus return `UnsupportedService` for non-empty menu lists.

## Context menus

Per-widget right-click menus are declared once and presented by the platform: the real OS menu opens at the pointer on all three desktops (`NSMenu` on macOS, `TrackPopupMenu` on Windows, `GtkPopoverMenu` on Linux); hosts without a native menu presenter (the mobile toolkit hosts and embed hosts today) mount the same declared items as an anchored canvas surface at the click point automatically. Markup declares them as a `<context-menu>` child of the pressable element; Zig views use `ElementOptions.context_menu`. Selections dispatch typed Msgs — see [Native UI](/docs/native-ui#native-scrolling-and-context-menus) for the authoring shapes and [Automation](/docs/automation) for driving them in tests (`widget-context-menu`).

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Native Controls
url: "https://native-sdk.dev/docs/native-controls"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Native controls are platform-owned views declared with `App.scene_fn`, attached with `runtime.createShellViews(...)`, created imperatively through `runtime.createView(...)`, or created from trusted WebView code through the guarded `window.zero.views.*` bridge helpers. They are intended for trusted chrome, utility panels, and OS-native affordances around rich WebView content.

For product UI, prefer Native SDK's [Built-in Components](/docs/built-in-components). They are native-rendered through the retained canvas/GPU surface, use shared design tokens, and stay portable across desktop and mobile hosts. Native controls remain useful for shell chrome, menus, platform dialogs, and OS-owned integration points.

```zig
const shell_views = [_]native_sdk.ShellView{
    .{ .label = "toolbar", .kind = .toolbar, .edge = .top, .height = 52 },
    .{ .label = "refresh-icon", .kind = .icon_button, .parent = "toolbar", .accessibility_label = "Refresh", .text = "R", .command = "app.refresh" },
    .{ .label = "refresh", .kind = .button, .parent = "toolbar", .text = "Refresh", .command = "app.refresh" },
    .{ .label = "search", .kind = .search_field, .parent = "toolbar", .text = "Search" },
    .{ .label = "live", .kind = .checkbox, .parent = "toolbar", .text = "Live" },
    .{ .label = "view-mode", .kind = .segmented_control, .parent = "toolbar", .text = "List|Grid" },
    .{ .label = "syncing", .kind = .progress_indicator, .parent = "toolbar", .role = "Syncing" },
    .{ .label = "body", .kind = .split, .fill = true, .axis = .row },
    .{ .label = "sidebar", .kind = .sidebar, .parent = "body", .width = 240 },
    .{ .label = "filters", .kind = .stack, .parent = "sidebar", .x = 16, .y = 16, .width = 180, .height = 120, .axis = .column },
    .{ .label = "filter-title", .kind = .label, .parent = "filters", .text = "Filters" },
    .{ .label = "inbox", .kind = .list_item, .parent = "filters", .text = "Inbox", .command = "app.open.inbox" },
    .{ .label = "filter-live", .kind = .checkbox, .parent = "filters", .text = "Live" },
    .{ .label = "main", .kind = .webview, .parent = "body", .url = "zero://inline", .fill = true },
};

const shell_windows = [_]native_sdk.ShellWindow{.{
    .label = "main",
    .title = "Acme",
    .width = 1100,
    .height = 760,
    .views = &shell_views,
}};

fn scene(context: *anyopaque) anyerror!native_sdk.ShellConfig {
    _ = context;
    return .{ .windows = &shell_windows };
}
```

## Supported Kinds

<table>
  <thead>
    <tr>
      <th>
        Kind
      </th>

      <th>
        Notes
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `toolbar`
      </td>

      <td>
        Native container for top app chrome
      </td>
    </tr>

    <tr>
      <td>
        `titlebar_accessory`
      </td>

      <td>
        Native titlebar-adjacent container where the platform allows it
      </td>
    </tr>

    <tr>
      <td>
        `sidebar`
      </td>

      <td>
        Native side panel container
      </td>
    </tr>

    <tr>
      <td>
        `statusbar`
      </td>

      <td>
        Native bottom/status container
      </td>
    </tr>

    <tr>
      <td>
        `split`
      </td>

      <td>
        Native container for fixed and fill panes; set

        `axis`

        to

        `.row`

        or

        `.column`
      </td>
    </tr>

    <tr>
      <td>
        `stack`
      </td>

      <td>
        Native container for grouped child controls; set

        `axis`

        to

        `.row`

        or

        `.column`
      </td>
    </tr>

    <tr>
      <td>
        `button`
      </td>

      <td>
        Dispatches

        `command`

        through

        `Event.command`
      </td>
    </tr>

    <tr>
      <td>
        `icon_button`
      </td>

      <td>
        Compact command button for toolbar-style actions
      </td>
    </tr>

    <tr>
      <td>
        `list_item`
      </td>

      <td>
        Row-like native command item for sidebars, navigators, and simple lists
      </td>
    </tr>

    <tr>
      <td>
        `checkbox`
      </td>

      <td>
        Native checkbox control
      </td>
    </tr>

    <tr>
      <td>
        `toggle`
      </td>

      <td>
        Native binary toggle button
      </td>
    </tr>

    <tr>
      <td>
        `segmented_control`
      </td>

      <td>
        Native segmented choice control; use pipe-delimited

        `text`

        , such as

        `List|Grid`
      </td>
    </tr>

    <tr>
      <td>
        `text_field`
      </td>

      <td>
        Native editable text field
      </td>
    </tr>

    <tr>
      <td>
        `search_field`
      </td>

      <td>
        Native search input
      </td>
    </tr>

    <tr>
      <td>
        `label`
      </td>

      <td>
        Native static text
      </td>
    </tr>

    <tr>
      <td>
        `spacer`
      </td>

      <td>
        Empty native layout spacer
      </td>
    </tr>

    <tr>
      <td>
        `progress_indicator`
      </td>

      <td>
        Native indeterminate progress indicator
      </td>
    </tr>

    <tr>
      <td>
        `webview`
      </td>

      <td>
        Compatibility view backed by the WebView backend
      </td>
    </tr>
  </tbody>
</table>

The macOS, Linux, and Windows system-WebView backends support these native kinds today. Chromium hosts return explicit unsupported errors until their native hosts implement the same surface.

## Layout

For declarative shell views, use `edge` for docked chrome, `fill = true` for content, `parent` for controls inside containers, `axis` for row or column child flow, and min/max size fields when a surface needs resize constraints. `stack` containers apply native spacing between controls; `split` containers divide panes along their axis without spacing, so a fixed sidebar and fill WebView can resize together. The runtime keeps shell views bound to the window and reapplies their frames on resize.

Use `accessibility_label` for the announced control name when it should differ from visible text. Use `role` for semantic/fallback accessibility text. Use `text` for visible labels, button titles, and text/search placeholders.

`ViewInfo.focused` reports the last successfully focused view in a window. Use `runtime.focusNextView(...)`, `runtime.focusPreviousView(...)`, or `window.zero.views.focusNext()` / `focusPrevious()` to move through visible, enabled native controls and WebView-backed views in stable view order. Automation snapshots include the same focus state for native controls and WebView-backed views.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Native Surfaces
url: "https://native-sdk.dev/docs/native-surfaces"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Native surfaces are the app-owned regions that the Native SDK composes inside a platform window. A surface can be a GPU-rendered canvas, native chrome, a native utility panel, or an embedded WebView. The key model is that a window is a declared tree of surfaces — the default app is one `gpu_surface` canvas filling the window, and everything else composes around it.

## Surface Types

<table>
  <thead>
    <tr>
      <th>
        Surface
      </th>

      <th>
        Use it for
      </th>

      <th>
        Backed by
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        GPU surface
      </td>

      <td>
        The canvas your

        <a href="/docs/native-ui">native UI</a>

        renders into — plus custom drawing regions for editors, timelines, dashboards, and games
      </td>

      <td>
        Metal-backed on macOS; retained Direct2D/DirectWrite on Windows; CPU reference-rendered on Linux and as the Windows fallback
      </td>
    </tr>

    <tr>
      <td>
        Native chrome
      </td>

      <td>
        Toolbars, titlebar accessories, sidebars, status bars
      </td>

      <td>
        Native platform views declared through

        `ShellView`

        or

        `runtime.createView(...)`
      </td>
    </tr>

    <tr>
      <td>
        Native controls
      </td>

      <td>
        Buttons, search fields, toggles, segmented controls, labels, progress
      </td>

      <td>
        Platform controls with command routing and accessibility metadata
      </td>
    </tr>

    <tr>
      <td>
        OS capability surfaces
      </td>

      <td>
        Dialogs, tray, clipboard, notifications, credentials, file drops
      </td>

      <td>
        Guarded

        `PlatformServices`

        , runtime methods, and built-in bridge commands
      </td>
    </tr>

    <tr>
      <td>
        WebView
      </td>

      <td>
        <a href="/docs/frontend">Embedded web content</a>

        : web frontends, documents, previews
      </td>

      <td>
        `ViewKind.webview`

        ,

        `WebViewSource`

        , and the selected

        <a href="/docs/web-engines">web engine</a>
      </td>
    </tr>
  </tbody>
</table>

## The default shape: one canvas

The generated app declares one window with one GPU surface, and the [`UiApp`](/docs/app-model) loop renders the whole UI into it:

```zig
const shell_views = [_]native_sdk.ShellView{
    .{ .label = "main-canvas", .kind = .gpu_surface, .fill = true, .gpu_backend = .metal },
};
const shell_windows = [_]native_sdk.ShellWindow{.{
    .label = "main",
    .title = "My App",
    .width = 480,
    .height = 320,
    .views = &shell_views,
}};
```

Everything below this section is composition around that canvas: platform chrome, split panes, and (when needed) web content.

## Window Shape

Use `App.scene_fn` (or `UiApp`'s `scene` option) when a window should start with native structure, such as chrome around a WebView workspace:

```zig
const shell_views = [_]native_sdk.ShellView{
    .{ .label = "toolbar", .kind = .toolbar, .edge = .top, .height = 52 },
    .{ .label = "refresh", .kind = .button, .parent = "toolbar", .accessibility_label = "Refresh workspace", .text = "Refresh", .command = "app.refresh" },
    .{ .label = "body", .kind = .split, .fill = true, .axis = .row },
    .{ .label = "sidebar", .kind = .sidebar, .parent = "body", .width = 240 },
    .{ .label = "main", .kind = .webview, .parent = "body", .url = "zero://inline", .fill = true },
    .{ .label = "status", .kind = .statusbar, .edge = .bottom, .height = 32, .text = "Ready" },
};

const shell_windows = [_]native_sdk.ShellWindow{.{
    .label = "main",
    .title = "Acme",
    .width = 1100,
    .height = 760,
    .views = &shell_views,
}};
```

This creates native chrome and controls in the platform host while the `main` WebView owns the workspace content.

## Canvas + WebView In One Window

A TypeScript canvas app exports `webPanes(model): readonly WebViewPane[]` to coordinate a manifest-declared child WebView with its canvas layout:

```ts title="src/core.ts"
import { asciiBytes } from "@native-sdk/core";
import type { WebViewPane } from "@native-sdk/core/events";

export function webPanes(model: Model): readonly WebViewPane[] {
  return [{ label: asciiBytes("preview"), anchor: asciiBytes("preview-pane"),
    x: 0, y: 0, width: 0, height: 0,
    url: asciiBytes(model.docs ? "https://native-sdk.dev/" : "https://example.com/"),
    reloadToken: model.reloadToken }];
}
```

Reserve the region in `app.native` with `<panel grow="1" label="preview-pane"/>`. The label names an existing WebView in the manifest; the anchor follows the widget's measured bounds. With `anchor: null`, the explicit frame applies. Changing the URL navigates, and changing `reloadToken` reloads. The first four descriptors apply in order. Geometry is finite f32 canvas points with nonnegative dimensions; tokens are nonnegative safe integers. The adapter copies labels (64 bytes), anchors (256 bytes), and URLs (the platform URL budget) into native storage. Navigation retains the manifest's origin policy. See the TypeScript [Canvas Preview example](https://github.com/vercel-labs/native/tree/main/examples/canvas-preview).

Zig cores and runtime extensions use the corresponding `UiApp.Options.web_panes` seam:

A canvas-first `UiApp` can host a live webview next to its native-rendered widgets — the "native, web, or both per window" composition. The scene declares the webview shell view (parent it to the canvas view so frames share the canvas coordinate space), and `UiApp.Options.web_panes` keeps it snapped to a canvas widget's layout frame and drives navigation from the model:

```zig
const shell_views = [_]native_sdk.ShellView{
    .{ .label = "preview-canvas", .kind = .gpu_surface, .fill = true, .gpu_backend = .metal },
    .{ .label = "preview", .kind = .webview, .parent = "preview-canvas", .url = "https://example.com/", .x = 240, .y = 76, .width = 704, .height = 548 },
};

fn view(ui: *Ui, model: *const Model) Ui.Node {
    return ui.row(.{ .grow = 1 }, .{
        sidebar(ui, model),
        // An empty panel reserves the webview region; its semantics label
        // is the pane anchor.
        ui.panel(.{ .grow = 1, .semantics = .{ .label = "preview-pane" } }, .{}),
    });
}

fn panes(model: *const Model, out: []PreviewApp.WebViewPane) usize {
    out[0] = .{
        .label = "preview",
        .anchor = "preview-pane",
        .url = model.url(),          // changing it navigates
        .reload_token = model.reload_token, // bumping it reloads
    };
    return 1;
}
```

Panes re-apply after every rebuild and presented frame, reconciling against the runtime's actual webview state, so shell relayouts (window restores, resizes) cannot leave the webview detached from its anchor. Pane URLs are subject to `security.navigation.allowed_origins`. A scene whose only webviews are children like this never grows an implicit `main` webview — the window stays canvas-first. The seam is engine-agnostic (it rides the same `PlatformServices` webview surface both the system and Chromium hosts implement), though `gpu_surface` canvases currently require the system engine on macOS. `examples/canvas-preview` demonstrates this, with a smoke test at `zig build test-canvas-preview-smoke`.

## Imperative View API

For dynamic surfaces, create and update views at runtime:

```zig
const info = try runtime.createView(.{
    .window_id = 1,
    .label = "sync-status",
    .kind = .label,
    .parent = "status",
    .accessibility_label = "Sync status",
    .text = "Syncing",
});

_ = try runtime.updateView(info.window_id, info.label, .{
    .text = "Synced",
});
```

Trusted WebView code can use the guarded bridge helpers when `js_window_api` or an explicit `builtin_bridge` policy allows view commands:

```js
const views = await window.zero.views.list();
await window.zero.views.update("status", { text: "Synced" });
await window.zero.commands.invoke("app.refresh");
```

## Command Routing

Surfaces should share command IDs instead of each UI entry point owning separate behavior. A native menu item, shortcut, toolbar button, tray item, native button, and WebView bridge call can all dispatch `app.refresh` into the same `Event.command` handler.

`CommandEvent.source` records whether the action came from `.menu`, `.shortcut`, `.toolbar`, `.tray`, `.native_view`, `.bridge`, or `.runtime`. `ViewInfo.id` is a stable runtime handle for the view lifetime, while `label` remains the selector used by runtime and bridge APIs.

## Compatibility

Existing `App.source`, `runtime.createWebView(...)`, and `window.zero.webviews.*` apps remain valid. The generic view model adds a wider vocabulary:

- `ViewKind.webview` wraps WebView-backed surfaces.
- `runtime.createView(.{ .kind = .webview, ... })` routes through the WebView backend.
- `runtime.listViews(...)` returns WebView-backed and native views through one list.
- Automation snapshots include native views and WebView-backed views.

## Platform Support

macOS, Linux, and Windows system hosts support native views and controls today. The macOS host presents `gpu_surface` through Metal, Windows presents retained representable packets through Direct2D/DirectWrite with an exact software fallback, and Linux presents through the software reference renderer. See `examples/gpu-surface` for a minimal native controls plus WebView plus GPU surface composition. Chromium hosts and mobile hosts expose the same public vocabulary where useful, but unsupported operations reject explicitly instead of silently falling back.

Use support checks before showing optional native affordances:

```zig
if (runtime.supports(.native_views)) {
    try runtime.createView(.{ .label = "toolbar", .kind = .toolbar });
}
if (runtime.supports(.gpu_surfaces)) {
    try runtime.createView(.{
        .label = "canvas",
        .kind = .gpu_surface,
        .frame = native_sdk.geometry.RectF.init(0, 0, 480, 320),
        .gpu_surface = .{
            .backend = .metal,
            .pixel_format = .bgra8_unorm,
            .present_mode = .timer,
            .alpha_mode = .@"opaque",
            .color_space = .srgb,
            .vsync = true,
        },
    });
}
```

```js
const hasNativeViews = await window.zero.platform.supports("native_views");
```

`gpu_surface` views report GPU presentation state through `ViewInfo` and `GpuSurfaceFrameEvent`, including the selected backend, pixel format, present mode, alpha mode, color space, vsync flag, status, frame size, frame index, timestamp, nonblank sample status, input latency budget, first-frame latency budget, retained canvas frame counters, widget revision, widget node count, and current cursor intent.

See [Platform Support](/docs/platform-support) for the current host matrix.

## Related Docs

- [App Model](/docs/app-model)
- [Native Controls](/docs/native-controls)
- [Commands](/docs/commands)
- [Capabilities](/docs/capabilities)
- [Multiple WebViews](/docs/webviews)
- [Config](/docs/app-zon)

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Native UI
url: "https://native-sdk.dev/docs/native-ui"
docs_index: /llms.txt
lastUpdated: 2026-10-18
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The default Native SDK app is native-rendered: the view is a Native markup file (`.native`), the logic is an app core — TypeScript by default, or Zig — and rendering happens through Native SDK's own engine. Scaffold one with `native init my_app` and run it with `native dev`.

An app is four things:

- `src/app.native` — the entire UI: elements, layout, bindings, and message dispatch.
- `Model` — a plain data structure holding your state.
- `Msg` — a tagged union of everything that can happen.
- `update(model, msg)` — the only place state changes.

The runtime owns installation, presentation, resize, event dispatch, timers, and hot reload. In `native dev`, `.native` edits refresh the running view while preserving the model and retained state of widgets whose identity remains stable. Zig views can register embedded fragments in `Options.fragment_watch` through `View.fragment("src/header.native")`. TypeScript apps use [compiled TypeScript views](/docs/typescript#compiled-typescript-views-preview) by default; `native dev` reloads their markup edits in place, and other direct runs of that backend need a rebuild after markup edits. Zig release builds compile markup at comptime and report invalid views with a line and column.

## A complete view

```html title="src/app.native"
<column gap="12" padding="16">
  <row gap="8" cross="center">
    <text-field text="{draft}" placeholder="New task…" on-input="draft_edit" on-submit="add" grow="1" />
    <button variant="primary" on-press="add">Add task</button>
  </row>
  <scroll grow="1">
    <column gap="2">
      <for each="visible" key="id" as="t">
        <row gap="8" padding="6" cross="center">
          <checkbox checked="{t.done}" on-toggle="toggle:{t.id}" label="Done" />
          <text grow="1">{t.title}</text>
        </row>
      </for>
    </column>
  </scroll>
  <status-bar>{openCount} open</status-bar>
</column>
```

## Elements

<table>
  <thead>
    <tr>
      <th>
        Markup
      </th>

      <th>
        Renders as
      </th>

      <th>
        Notes
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `row`

        ,

        `column`
      </td>

      <td>
        flex containers
      </td>

      <td>
        horizontal / vertical main axis
      </td>
    </tr>

    <tr>
      <td>
        `stack`

        ,

        `panel`

        ,

        `card`
      </td>

      <td>
        overlay containers
      </td>

      <td>
        children layer on top of each other —

        `gap`

        can never space them and is a validation error (put a

        `column`

        /

        `row`

        inside for flow)
      </td>
    </tr>

    <tr>
      <td>
        `scroll`
      </td>

      <td>
        scroll view
      </td>

      <td>
        wrap multiple children in a

        `column`

        inside it
      </td>
    </tr>

    <tr>
      <td>
        `list`

        ,

        `grid`
      </td>

      <td>
        list, grid
      </td>

      <td>
        vertical stack / cell grid
      </td>
    </tr>

    <tr>
      <td>
        `tabs`

        ,

        `toggle-group`

        ,

        `button-group`

        ,

        `radio-group`

        ,

        `breadcrumb`

        ,

        `pagination`
      </td>

      <td>
        row containers
      </td>

      <td>
        children flow horizontally (tab buttons, toggle-buttons, radios, ...)
      </td>
    </tr>

    <tr>
      <td>
        `table`

        \>

        `table-row`

        \>

        `table-cell`
      </td>

      <td>
        table
      </td>

      <td>
        rows only inside a table, cells only inside a row (

        `for`

        /

        `if`

        wrappers are fine); cells are text leaves
      </td>
    </tr>

    <tr>
      <td>
        `dropdown-menu`
      </td>

      <td>
        menu surface
      </td>

      <td>
        vertical; children are

        `menu-item`

        s.

        `anchor="below|above"`

        floats it against its parent's frame: a late z-pass above the whole tree, window-clipped (never cropped by a scroll pane), auto-flipping at the window edges, zero flow space — pair with

        `on-dismiss`

        so Escape/click-outside close model-side
      </td>
    </tr>

    <tr>
      <td>
        `accordion`
      </td>

      <td>
        accordion
      </td>

      <td>
        header via the

        `text`

        attribute; children show while

        `selected`

        , dispatch

        `on-toggle`
      </td>
    </tr>

    <tr>
      <td>
        `alert`

        ,

        `bubble`
      </td>

      <td>
        surfaces
      </td>

      <td>
        `alert`

        title via the

        `text`

        attribute; children stack inside.

        `bubble`

        hugs its message up to 80% of the thread (

        `ghost`

        exempt; explicit

        `width`

        wins) and takes one

        `<reactions>`

        child — the reaction pill straddling its bottom edge, one text run, dock via

        `text-alignment`

        (default

        `end`

        );

        `text=`

        on bubble itself is a teaching error (that channel belongs to the pill)
      </td>
    </tr>

    <tr>
      <td>
        `dialog`

        ,

        `drawer`

        ,

        `sheet`
      </td>

      <td>
        modal surfaces
      </td>

      <td>
        root-relative and unaffected by ancestor scroll or clipping: dialogs center in the viewport, drawers span its width and dock to the bottom, and sheets span its height and dock to the right — title via

        `text`

        , wrap in

        `<if>`

        to show conditionally
      </td>
    </tr>

    <tr>
      <td>
        `resizable`
      </td>

      <td>
        resizable panel
      </td>

      <td>
        engine-managed drag handle;

        `width`

        sets the initial width
      </td>
    </tr>

    <tr>
      <td>
        `split`
      </td>

      <td>
        two-pane splitter
      </td>

      <td>
        exactly two element children (nest splits for more panes); the engine synthesizes the draggable divider between them.

        `value`

        binds the model-owned first-pane fraction,

        `on-resize`

        names an f32 Msg variant dispatched with each applied fraction (echo it back through

        `value`

        ),

        `min-width`

        on the panes bounds the drag,

        `gap`

        sets the divider band thickness; the focused divider takes Left/Right (Shift for bigger steps) and Home/End.

        `resize-duration`

        (milliseconds, split only) animates model-driven value moves — the runtime eases the rendered fraction to the new value one presented frame at a time instead of snapping, and reduced-motion appearances snap automatically;

        `resize-easing`

        (

        `linear`

        /

        `standard`

        /

        `emphasized`

        /

        `spring`

        ) shapes the ramp and needs a nonzero duration beside it
      </td>
    </tr>

    <tr>
      <td>
        `tree`
      </td>

      <td>
        disclosure tree
      </td>

      <td>
        vertical container whose descendant rows with

        `role="treeitem"`

        form one roving keyboard focus set: Up/Down walk visible rows (selection follows focus via

        `on-change`

        when bound, otherwise

        `on-press`

        ), Left collapses or moves to the parent row, Right expands or moves to the first child row, Home/End jump to the edges, Enter/Space activate; nested rows derive hierarchy structurally, while flat

        `<for>`

        rows declare their one-based

        `tree-level`

        ; expandable rows bind

        `expanded`

        and

        `on-toggle`

        , and the model owns both states
      </td>
    </tr>

    <tr>
      <td>
        `text`

        ,

        `badge`

        ,

        `tooltip`
      </td>

      <td>
        text leaves
      </td>

      <td>
        `{}`

        interpolation allowed in content;

        `text`

        takes a line policy via

        `wrap`

        (

        `"true"`

        word-wraps;

        `"false"`

        /unset paint a single line) and an overflow policy via

        `overflow`

        (

        `ellipsis`

        — the default trailing elision — or

        `clip`

        ) and the typography rungs via

        `size="heading"`

        /

        `size="display"`

        (themable token steps above title — section headings, hero stats).

        `tooltip`

        with

        `anchor="above|below"`

        floats against its parent — the

        `stack`

        wrapping trigger + tooltip, the same shape as the anchored

        `dropdown-menu`

        — and the runtime owns its visibility through hover intent: it shows after the pointer rests on the trigger for

        `tooltip-delay`

        milliseconds (default 600;

        `"0"`

        shows instantly), skips the delay during the shared 400ms warm window after a pointer-hovered tooltip hides on leave, and shows immediately when the trigger gains keyboard focus; it hides on pointer leave (the only hide that warms), focus departure, Escape, or a press — the model never hears hover. Without

        `anchor`

        it stays a static leaf that paints whenever the view renders it — see

        <a href="/docs/components/tooltip">Tooltip</a>
      </td>
    </tr>

    <tr>
      <td>
        `text`

        \>

        `span`
      </td>

      <td>
        inline styled runs
      </td>

      <td>
        mixed-style text in one wrapped paragraph: span children style runs with

        `weight="regular|medium|bold"`

        ,

        `mono`

        ,

        `italic`

        ,

        `scale`

        (a positive multiplier on the paragraph's base size — inline headings, hero stats),

        `underline`

        ,

        `foreground`

        (a token name);

        `{bindings}`

        interpolate inside spans; whitespace between runs collapses to a single space (none = the runs abut); spans do not nest, take no events, and the paragraph announces as one text run — see

        <a href="#rich-text-inline-spans-and-markdown">Rich text</a>
      </td>
    </tr>

    <tr>
      <td>
        `button`

        ,

        `toggle-button`

        ,

        `list-item`

        ,

        `menu-item`

        ,

        `toggle`

        ,

        `switch`

        ,

        `select`

        ,

        `avatar`
      </td>

      <td>
        text-bearing controls
      </td>

      <td>
        the label is the element content;

        `button`

        ,

        `toggle-button`

        ,

        `list-item`

        , and

        `menu-item`

        also take

        `icon="..."`

        — a vector icon drawn inline (buttons and toggle-buttons before the label, icon-only when the content is empty; list and menu items as a leading slot), one hit target that follows the element's enabled/disabled tint; tab strips use

        `button`

        children, lowered to segmented controls;

        `select`

        shows its

        `placeholder`

        while empty and dispatches

        `on-press`

        ;

        `avatar`

        renders initials, or a runtime image via

        `image="{binding}"`

        .

        `list-item`

        alone also accepts element children IN PLACE of the text run (the list-row composite): the children flow horizontally inside the row's flat chrome — transparent at rest, full-width hover/selected washes, one hit target — so composite rows (title + snippet, leading icon + trailing badge) remain list rows instead of bordered boxes; name the row with

        `label`

        and never mix a text run with children. Rows that really are cards (a kanban board's draggable cards) opt into the card treatment explicitly with

        `background`

        /

        `radius`
      </td>
    </tr>

    <tr>
      <td>
        `checkbox`

        ,

        `radio`

        ,

        `slider`

        ,

        `progress`
      </td>

      <td>
        value controls
      </td>

      <td>
        `checked`

        ,

        `value`
      </td>
    </tr>

    <tr>
      <td>
        `text-field`

        ,

        `input`

        ,

        `search-field`

        ,

        `combobox`

        ,

        `textarea`
      </td>

      <td>
        text entry
      </td>

      <td>
        `placeholder`

        ; edits arrive via

        `on-input`
      </td>
    </tr>

    <tr>
      <td>
        `status-bar`
      </td>

      <td>
        status bar
      </td>

      <td>
        content only, no children
      </td>
    </tr>

    <tr>
      <td>
        `separator`

        ,

        `spacer`
      </td>

      <td>
        separator, flexible space
      </td>

      <td>
        `separator`

        is axis-aware — a horizontal rule in a

        `column`

        , a thin vertical divider in a

        `row`

        ; give

        `spacer`

        a

        `grow`
      </td>
    </tr>

    <tr>
      <td>
        `skeleton`

        ,

        `spinner`
      </td>

      <td>
        loading leaves
      </td>

      <td>
        size

        `skeleton`

        with

        `width`

        /

        `height`
      </td>
    </tr>

    <tr>
      <td>
        `icon`
      </td>

      <td>
        vector icon leaf
      </td>

      <td>
        `name`

        picks the icon: a bare literal is one of the curated stroke-dialect built-ins (compile-checked; including transport, files/folders, arrows/chevrons, panes (panel-left, panel-right), tooling (terminal, wrench), theming, and the full GitHub vocabulary (git-pull-request, git-merge, git-branch, circle-dot, check-circle, x-circle));

        `app:<name>`

        reaches an icon the app registered at boot (verified by

        `native check`

        against the model contract), and one

        `{binding}`

        defers the choice to model data (an unknown resolved name draws the missing-icon fallback with a Debug warning naming the value - never a silent gap); tint with

        `foreground`

        , size with

        `width`

        /

        `height`

        ; never a press target — decoration inside pressable rows passes clicks through to the row (give the icon its own

        `on-press`

        to make it an icon hotspot)
      </td>
    </tr>

    <tr>
      <td>
        `markdown`
      </td>

      <td>
        rendered markdown
      </td>

      <td>
        leaf;

        `source`

        is one

        `{binding}`

        — see

        <a href="#rich-text-inline-spans-and-markdown">Rich text</a>
      </td>
    </tr>

    <tr>
      <td>
        `media-surface`
      </td>

      <td>
        media surface
      </td>

      <td>
        composites a texture produced OUTSIDE the widget tree (video, camera, an external renderer) like any widget;

        `surface="{binding}"`

        (required) binds the model-owned

        `u64`

        surface id a Zig-tier producer targets — no intrinsic size, display-only,

        `label`

        it; goldens and replay see the deterministic id-derived placeholder, never producer frames — see

        <a href="/docs/components/media-surface">Media Surface</a>

        and

        <a href="/docs/media-producers">Media Producers</a>
      </td>
    </tr>

    <tr>
      <td>
        `video`
      </td>

      <td>
        video playback
      </td>

      <td>
        leaf;

        `src`

        declares the app's single platform-decoded playback (an assets path or

        `http(s)`

        URL, local-first like audio),

        `autoplay`

        /

        `loop`

        /

        `muted`

        shape it, and

        `controls`

        composes the house transport chrome — without it the element is the surface alone and your update code drives the video command vocabulary; no intrinsic size,

        `label`

        it; goldens and replay see the placeholder, transport is journaled truth — see

        <a href="/docs/components/video">Video</a>
      </td>
    </tr>

    <tr>
      <td>
        `stepper`

        \>

        `step`
      </td>

      <td>
        stage stepper
      </td>

      <td>
        `active="{index}"`

        derives each step's completed/active/pending state; steps are text leaves joined by connectors — see

        <a href="#pipeline-components-stepper-timeline-and-nav">Pipeline components</a>
      </td>
    </tr>

    <tr>
      <td>
        `timeline`

        \>

        `timeline-item`
      </td>

      <td>
        ledger/timeline list
      </td>

      <td>
        items are leaves whose

        `title`

        /

        `description`

        /

        `meta`

        /

        `indicator`

        attributes carry the content;

        `on-press`

        makes the whole item pressable
      </td>
    </tr>

    <tr>
      <td>
        `segmented-control`
      </td>

      <td>
        exclusive-choice control
      </td>

      <td>
        standalone text-bearing segment; bind

        `selected`

        , dispatch

        `on-press`

        , and optionally add a vector

        `icon`

        ; compose several in a row, or use

        <a href="/docs/components/tabs">tabs</a>

        for the strip-and-panel pattern
      </td>
    </tr>

    <tr>
      <td>
        `chart`

        \>

        `series`
      </td>

      <td>
        data chart
      </td>

      <td>
        each series binds a model

        `[]const f32`

        iterable via

        `values="{binding}"`

        (

        `kind`

        is

        `line`

        /

        `area`

        /

        `bar`

        ,

        `color`

        a token name);

        `y-min`

        /

        `y-max`

        /

        `grid-lines`

        /

        `baseline`

        shape the plot,

        `x-labels`

        /

        `y-labels`

        add muted axis ticks,

        `hover-details`

        adds the pointer detail card — see

        <a href="#charts">Charts</a>
      </td>
    </tr>

    <tr>
      <td>
        `input-group`

        \>

        `textarea`

        \+

        `input-group-actions`
      </td>

      <td>
        grouped input
      </td>

      <td>
        the composer shape: ONE bordered field wrapping exactly one

        `textarea`

        (first) plus an optional

        `input-group-actions`

        row of controls inside the same border — the group wears the focus ring for its focused descendant and the textarea's own chrome dissolves; put a

        `<spacer grow="1"/>`

        between leading and trailing controls — see

        <a href="/docs/components/input-group">Input Group</a>
      </td>
    </tr>
  </tbody>
</table>

A tab strip, a select trigger, and a table read the way you would guess:

```html
<tabs gap="4">
  <button selected="{tab == overview_tab}" on-press="set_tab:{overview_tab}">Overview</button>
  <button selected="{tab == data_tab}" on-press="set_tab:{data_tab}">Data</button>
</tabs>
<select placeholder="Pick a fruit" on-press="open_picker">{choice}</select>
<!-- the options: an if-gated <dropdown-menu anchor="below" on-dismiss="close_picker"> beside the select in a stack -->
<table gap="2">
  <for each="rows" key="id" as="r">
    <table-row gap="4">
      <table-cell on-press="pick_row:{r.id}">{r.name}</table-cell>
      <table-cell>{r.qty}</table-cell>
    </table-row>
  </for>
</table>
```

Use `<data-grid>` with `<table-row>` and `<table-cell>` for retained grids, `<popover>` for a floating panel, and `<menu-surface>` for menu composition. Popovers and menu surfaces support `anchor` and `on-dismiss`; `backdrop-blur="none|sm|md"` selects the token-backed blur for a surface and also accepts a bound token. Vector icon buttons use `<button size="icon" icon="..." label="..." on-press="..." />`. Runtime image bindings are available on `avatar` and `image`; per-state styles beyond design tokens use the Zig builder.

Apps with their own iconography can parse any stroke-dialect/Feather/Tabler-dialect SVG at comptime (`canvas.svg_icon.parseComptime(@embedFile("icons/logo.svg"))`) and register it at boot with `canvas.icons.registerAppIcons(&table)`: the draw paths (icon leaves via `ui.appIcon`, `ElementOptions.icon` on buttons, toggle buttons, icon buttons, and list/menu items) resolve registered names exactly like built-ins. Markup reaches them through the `app:` NAMESPACE (`<icon name="app:waveform"/>`, `icon="app:waveform"`): bare names keep the closed built-in vocabulary (the compiled engine proves them at comptime, where a runtime registration cannot exist), while `app:` names are structurally accepted by both engines and verified by `native check` against the model contract - declare the table as `pub const app_icons` on the app root so the contract emit reflects the same names `main` registers. Bound icon names (`icon="{binding}"`) make the choice model data - a per-row status icon, a play/pause toggle - and any name that fails to resolve at draw time renders the missing-icon fallback (a slashed circle) with a Debug warning naming the value, never a silent gap.

**Layout.** `gap`, `padding`, `grow`, `width`, `height`, `min-width`, `max-width`, `main`, `cross`, and `columns` control sizing and flow. `gap` requires a flow container; put a row or column inside an overlay surface. On `split`, it sets divider thickness. `width` and `height` set definite sizes (`resizable` treats width as its initial size); `min-width` is a floor and `max-width` a ceiling. `columns` applies only to grids. Debug builds report `zero_canvas_layout` when child minimum sizes exceed a container.

**Text.** `wrap="true"` wraps and reserves the measured height. Single-line text defaults to `overflow="ellipsis"`; `clip` cuts at the frame. `text-alignment="start|center|end"` aligns leaves, status bars, and surface titles. `size` selects `default`, `sm`, `lg`, or `icon`; text also accepts `heading` and `display`. Change typography tokens for other sizes.

**Floating surfaces.** `anchor="below|above"` positions a dropdown menu or tooltip against its parent and flips it at window edges. `anchor-alignment="start|end|stretch"` and `anchor-offset` (4 points by default) adjust placement. Anchored tooltips have runtime-owned visibility and `tooltip-delay` (600 ms by default); keyboard focus shows them immediately. Dropdown visibility belongs in the model.

**Control state.** `variant`, `disabled`, `checked`, `selected`, and tree-row `expanded` set presentation and state. `autofocus` moves focus on mount or when its bound value changes to true.

**Identity and semantics.** `key` preserves sibling identity; `global-key` preserves identity across containers. `label` replaces the accessible name derived from visible text. `role="treeitem"` joins a tree's roving focus group; flat sibling rows declare their one-based depth through `tree-level`.

**Window chrome.** `window-drag="true"` creates a [drag region](#hidden-titlebar-drag-regions-and-chrome-insets).

## Styling with design tokens

Color and radius attributes reference design tokens by name — literals only, validated against the token lists (a typo is a check/compile error, never a silent default):

```html
<row background="surface" radius="md" padding="8">
  <text foreground="text_muted">Muted caption</text>
</row>
```

The color attributes are `background`, `foreground`, `accent`, `accent-foreground`, `border-color`, and `focus-ring`; values are `ColorTokens` field names: `background`, `surface`, `surface_subtle`, `surface_pressed`, `text`, `text_muted`, `syntax_plain`, `syntax_comment`, `syntax_keyword`, `syntax_literal`, `syntax_function`, `syntax_property`, `syntax_constant`, `border`, `accent`, `accent_text`, `destructive`, `destructive_text`, `success`, `success_text`, `warning`, `warning_text`, `info`, `info_text`, `focus_ring`, `shadow`, `scrim`, `disabled`. The `syntax_*` roles are the Geist Code Block palette shared by both built-in packs; `ui.code` and Markdown fences use them automatically. `info` is the violet identity hue beside the status trio — merged PR badges, "new" chips, informational callouts. `radius` takes a `RadiusTokens` name: `sm`, `md`, `lg`, `xl`, `none`.

References resolve against the app's live tokens on every rebuild, so themed apps re-resolve them when the theme changes — a scheme change updates `surface`. Anything dynamic beyond that (raw colors, per-state styling) stays in Zig via `ElementOptions.style`, which always wins over a token reference.

By default the stock tokens follow the system appearance: an app that sets neither `tokens` nor `tokens_fn` derives its theme from the OS light/dark setting (plus high-contrast and reduce-motion) and re-themes live when the user flips it — no restart, no wiring. Apps that own their look opt out by passing explicit `tokens` (a fixed theme) or `tokens_fn` (model-owned theming — the pattern for a custom palette that still follows the system scheme through `on_appearance`).

## Structure tags

`<for each="..." as="...">` repeats its body per item and `<if test="{...}">` renders its children conditionally. A `for` body takes one or more children — elements, `<use>`, `<if>`/`<else>` arms, or nested `<for>`s — so polymorphic rows need no wrapper node. `key` names an item field: every node an item emits shares the item's identity across reorders (same-kind siblings within one item are disambiguated automatically), and a node's own `key`/`global-key` still wins. An `<else>` directly after an `</if>` renders when the test is false; directly after a `</for>` it renders the empty state when the iterable has no items:

```html
<for each="visible" key="id" as="t">
  <if test="{t.done}"> <badge>done</badge> </if>
  <else> <text>{t.title}</text> </else>
</for>
<else> <text>Nothing yet</text> </else>
```

There is no `else-if` chain tag — nest an `<if>`/`<else>` inside the `<else>` body instead.

## Templates

When the same subtree repeats with different data, define it once — templates go at the top of the file, before the single view root:

```html
<template name="board-column" args="title cards">
  <column grow="1" gap="8" label="{title}">
    <text foreground="text_muted">{title}</text>
    <for each="cards" key="id" as="c">
      <row global-key="{c.id}"><text>{c.title}</text></row>
    </for>
  </column>
</template>
<row grow="1" gap="12">
  <use template="board-column" title="Todo"  cards="{todoCards}" />
  <use template="board-column" title="Doing" cards="{doingCards}" />
  <use template="board-column" title="Done"  cards="{doneCards}" />
</row>
```

A `<use>` is allowed anywhere an element is, and its attributes must match the template's declared `args` exactly. Args bind like `for` variables: a `{binding}` naming an iterable (the same set `for each` accepts) is iterable inside the template; anything else — a literal or a scalar binding — is a value for bindings and interpolation. The body is built in place of the `<use>`, so widget ids are identical to writing it out by hand: converting copy-pasted markup to a template changes no ids. Templates may only reference templates defined earlier in the file, which keeps expansion finite; bindings stay zero-argument, so per-case queries (`todoCards`, `doingCards`) remain named model functions.

An arg can declare a literal default with `name=value`, and a use site may then omit it — `args="title trend=flat"` makes `trend` optional. Defaults are literals only: they evaluate in no scope, so a `{binding}` default is rejected with a teaching error, and a missing arg without a default keeps its error too. `args="name="` declares an empty-string default, and defaults are unquoted — quotes in a default would be literal characters, so a quoted default (`name='x'`) is rejected with a teaching error.

## Components

Templates become shared components through `<import>`, which goes at the very top of a file, before its own templates:

```html
<import src="components/board-column.native"/>
<column gap="12">
  <use template="board-column" title="Todo" cards="{todoCards}" />
</column>
```

Paths resolve relative to the importing file, subdirectories and transitive imports included, and everything must stay under the markup root (the root view file's directory) — absolute paths and escapes are rejected. An imported file defines templates only: it is a component file, valid on its own for `native markup check`, and a view root inside one is an error. Importing splices the file's templates (and transitively its imports') before yours, in import order — exactly as if pasted at the import site — so the define-before-use rule is the only ordering rule to learn. Cycles are reported with the cycle path spelled out, and a template name defined twice is an error naming both definition sites: imports never shadow silently.

In a default TypeScript app, `src/app.native` may import any component file under `src/`. The generated build discovers and embeds every other `.native` file for the runtime and compiled engines, including mobile builds. `native dev` re-resolves the imported closure from disk, so editing a component file refreshes the view without restarting. Secondary-window roots keep their narrower `src/windows/` import root.

A template can also take markup children. The body marks the insertion point with a single `<slot/>`, and the use site's children build in the consumer's scope — they see the model paths and loop variables where the `<use>` is written — then land at the slot's position. A `<use>` may also carry `on-press="tag"` or `on-press="tag:{binding}"` to forward a typed press to the expanded root:

```html
<template name="section" args="title">
  <column gap="4">
    <text foreground="text_muted">{title}</text>
    <slot/>
  </column>
</template>
<use template="section" title="Cards">
  <for each="cards" as="c"> <text>{c.title}</text> </for>
</use>
```

A use with no children renders the slot empty; children passed to a template without a slot are an error; a template body takes at most one `<slot/>` (named slots are not supported yet). Structural widget ids hash exactly as if the slot content were written inline, in both engines.

## Expressions

Attribute values take a literal or exactly one `{expression}`; text content interpolates any number of them (`{openCount} open`). An expression is a binding path (`{c.title}`) or a pure, total combination of paths, literals (numbers, `'strings'` in single quotes, `true`/`false`), operators, and the built-in function library — spreadsheet power, never a programming language: no user-defined functions, no effects, no computed message names, and every expression terminates by construction.

Operators: arithmetic `+ - * /` (division always produces a float — wrap in `round()`/`floor()`/`ceil()` for whole-number attributes; dividing by zero is a loud error, never a silent zero or NaN), comparison `== != < <= > >=` (ordering takes numbers only; equality compares any two values, and different types are simply not equal), boolean `and`/`or`/`not` (booleans only — write `count > 0`, not `count`), and `++` (joins anything as display text, formatted exactly like interpolation). Comparisons do not chain, and `and`/`or` evaluate both sides (expressions are pure, so only errors are observable).

Type discipline is teaching errors over silent coercion: a string minus a number is an error at check/compile time when the types are known, and a loud build failure when they are not — never a NaN. Both engines evaluate through one shared evaluator, so results (float formatting included) are identical bit for bit.

The function library is closed — seventeen functions, and growing the set is a toolkit change: `fixed(x, digits)` (exact decimals), `thousands(n)` (`1,234,567`), `percent(fraction, digits?)` (`0.42` → `42%`), `date(ts)`/`time(ts)`/`datetime(ts)` (a model unix timestamp in seconds, formatted in UTC — formatting model time is pure; *reading the clock* is an effect, so `now()` is a teaching error pointing at the model/fx loop), `upper`/`lower`/`trim` (ASCII case mapping; other characters pass through), `min`/`max`/`abs`, `round`/`floor`/`ceil` (number → whole number), `plural(count, singular, plural)` (`{plural(n, 'item', 'items')}`), and `pad(x, width)` (zero-pads the integer value of x to `width` digits — `pad(7, 2)` → `07`, a negative sign precedes the zeros without counting toward the width, and numbers wider than `width` print in full; the mm:ss counter function: `{pad(minutes, 2)}:{pad(seconds, 2)}`).

Complexity is bounded and taught one past the bound: at most 256 bytes, 64 terms, and 16 nesting levels per expression — anything larger is a named core helper by design.

The same line separates inline arithmetic from core helpers. Inline expression arithmetic is sanctioned for one-off presentation-level derivation — `{percent(done / total)}` on the single readout that shows it is exactly what expressions are for. The moment a derivation is reused in a second binding, deserves a name, or carries meaning the model owns (a threshold, a rule, a policy), it belongs in a named helper: `{completionRate}` reads at the binding site, tests in the core's language, and changes in one place.

In the default TypeScript core, model fields bind by their authored names (`nextId` is `{nextId}`), and an exported helper declared in `src/core.ts` with exactly one `Model` parameter becomes a derived binding under its exported name. Scalar-returning helpers drive text/attributes; array-returning helpers drive `for each` and chart series; item record fields continue the path. See [TypeScript Cores](/docs/typescript) for the supported return shapes and module boundary.

In a Zig core, bindings resolve struct fields and zero-argument public model methods. A `for each` iterable may also be a slice, public array declaration, `pub fn (*const Model) []const T`, or `pub fn (*const Model, std.mem.Allocator) []const T`; the allocator form derives filtered rows into the one-build arena. Scalar bindings take that form too: `{summary}` binds `pub fn summary(m: *const Model, arena: std.mem.Allocator) []const u8`. Arena-computed values work in text, attributes, message payloads, and function arguments, but not as comparison operands — compare source fields or bind a boolean helper. In either tier, write `<if test>` predicates explicitly (`test="{count > 0}"` or `test="{hasItems}"`) instead of leaning on numeric truthiness. Expressions are allowed in text interpolation, attribute values, `if` tests, and template args at use sites; message tags and payloads, `for each` iterables, and import paths stay path-only.

## Messages

`on-press`, `on-toggle`, `on-change`, and `on-submit` name a variant of your `Msg` union, optionally with one binding payload (`toggle:{t.id}`). `on-input` names a variant carrying the runtime's text-input event, and each core tier declares that in its own vocabulary. In a TypeScript core the event union mirrors the runtime's event vocabulary structurally, and `on-input` matches the shape and translates each runtime event into your union at dispatch — import it from `@native-sdk/core/text` (or `@native-sdk/core/events`), or declare the same shape in-file; your `update` reduces the events over the draft bytes. In a Zig core the payload is `canvas.TextInputEvent`; pair it with `canvas.TextBuffer` in the model for editor state that survives rebuilds — the markup binds a `pub fn` returning the text (`text="{draft}"`), and binding the `TextBuffer` field itself directly is a teaching error pointing at this pattern:

```ts
import { type TextInputEvent } from "@native-sdk/core/text";
// insert_text / delete_* / clear / move_caret / set_selection /
// set_composition / commit_composition / cancel_composition — the full
// union is the SDK's; the same shape declared in-file matches identically.

export type Msg =
  | { readonly kind: "add" }
  | { readonly kind: "draft_edit"; readonly edit: TextInputEvent };
```

```zig
draft_buffer: canvas.TextBuffer(64) = .{},
pub fn draft(model: *const Model) []const u8 {
    return model.draft_buffer.text();
}
// in update:
.draft_edit => |edit| model.draft_buffer.apply(edit),
.add => { model.addTask(model.draft_buffer.text()); model.draft_buffer.clear(); },
```

`on-scroll` on a `scroll` element names a Msg carrying `ScrollState` from `@native-sdk/core/events`. Its eight numeric fields are `offsetX`, `offsetY`, `velocityX`, `velocityY`, `viewportExtentX`, `viewportExtentY`, `contentExtentX`, and `contentExtentY`. Zig uses `canvas.ScrollState` with snake\_case field names and `Ui.scrollMsg(.tag)`. The event reports the state already applied by wheel, momentum, keyboard, or accessibility scrolling. Echo `offsetY` through `value` and `offsetX` through `value-x` for a controlled region.

`on-resize` (on the `split` element; `Ui.valueMsg(.tag)` on `on_resize` in Zig views) names a variant whose payload is the new first-pane fraction (`f32`): after every divider drag, keyboard adjustment, or assistive increment/decrement the runtime delivers the fraction it already applied and clamped — store it in the model and echo it back through the split's `value`, and rebuilds never fight live resizing.

`on-drag` makes any element a draggable spatial object and names the closed record `{ sourceId, phase, x, y, viewWidth, viewHeight }`. Markup supplies numeric `sourceId` from a binding such as `on-drag="card_dragged:{card.id}"`; the runtime supplies `phase` (a number able to represent 0, 1, and 2) plus floating-point view-local `x`, `y`, `viewWidth`, and `viewHeight` (`f32`/`f64` in Zig, `number` in TypeScript). Geometry stays floating-point because pointer capture can carry negative coordinates outside the view. Phase 0 means motion, 1 release, and 2 cancellation. The renderer lifts the actual source appearance under the pointer at full opacity and leaves its in-flow space blank. Apps that need precise insertion can keep committed data unchanged during phase 0 while returning a derived view that moves the same `global-key` into the candidate position. Its hidden in-flow rendering is the one card-sized reserved slot: it begins at the source, moves to each candidate, and never duplicates. Keyed draggable neighbors ease between candidate poses; release carries the floating item from its pointer position into that slot and commits the exact order. A plain Escape during the drag dispatches phase 2, consumes that key, and carries the item back to its source slot; pointer cancellation uses the same path. Reduced-motion appearances snap these reflows. `examples/kanban` demonstrates within-column and cross-column reordering with this pattern.

`on-dismiss` (on the dismissible surfaces: `dialog`, `drawer`, `sheet`, `dropdown-menu`; `ElementOptions.on_dismiss` in Zig views) dispatches when Escape or a click outside dismisses the surface, so the model owns the close — clear the open flag in `update`. The engine hides the surface immediately as an optimistic echo; the next rebuild's source tree is truth. Escape works regardless of focus: it dismisses the nearest surface up the focused widget's chain, and when nothing relevant is focused — a menu opened from a plain-text trigger takes no focus — it falls back to the topmost mounted anchored surface. `on-hold` (any element; `ElementOptions.on_hold`) is press-and-hold: a pointer held \~350 ms dispatches the hold Msg and the release presses nothing, a quick click dispatches `on-press` as usual, and a right/ctrl-click with no context menu on its route dispatches the hold Msg immediately (a declared `<context-menu>` always wins the right-click) — the crumb-switcher shape (`on-press` selects, `on-hold` opens an anchored menu). Both legs are live-drivable through automation: `native automate widget-hold <view> <id>` runs the pointer+timer gesture, `widget-context-press <view> <id>` the secondary click.

`on-hover-enter` and `on-hover-leave` dispatch containment changes from mouse or trackpad motion. Binding them makes an element hover-hittable, without adding press behavior, a control hover wash, or an accessibility action. Nested enters run outermost first; leaves run innermost first. Scrolling, reflow, unmounting, and dismissal can cause leaves under a stationary pointer. Overlay occlusion follows hit testing. Touch does not synthesize hover, so provide another way to reach actions or content shown on hover.

`on-double-press` (`ElementOptions.on_double_press` in Zig) handles double clicks. Primary presses within 500 ms and 4 points increase the click count, capped at three. The first release dispatches `on-press`; the second and third dispatch `on-double-press` instead. Without a double handler, each click uses `on-press`. For a list row, pair select-on-press with open-on-double-press, and bind the same open action to `on-submit` for keyboard users.

On a ring-focused `list-item`, Enter dispatches `on-submit` when bound; Space dispatches the select action. Without a submit handler, both keys activate selection. Pointer-focused plain rows follow the [quiet-focus rules](#keyboard-routing-focus-registers-quiet-list-rows-and-the-app-level-fallback).

Presses follow one rule: a click lands on the nearest press-claiming widget under the pointer — plain text, icons, images, badges, and layout containers let it fall through to their closest interactive ancestor, and dragging still selects text. Any element with `on-press`, `on-double-press`, `on-toggle`, `on-hold`, or `on-drag` becomes a hit target and press claimer, so a row can bind `on-press` for clicks or `on-drag` for spatial dragging and child text still resolves to that row without overlays or duplicated handlers. Nested claimers resolve to the deepest one; editable text fields, scroll containers, and modal surfaces always claim their own presses. Value/text handlers (`on-change`, `on-submit`, `on-input`) still belong on controls only — the validator, both engines, and the LSP reject them on layout/decoration elements with a teaching error.

## Keyboard routing: focus registers, quiet list rows, and the app-level fallback

Keyboard focus has two registers, and the split is what makes desktop keyboard conventions expressible without per-widget wiring:

- **Ring focus** is the keyboard contract: Tab (and Shift+Tab) walk the focusable widgets and draw the visible focus ring. A ring-focused widget owns its keys in full — Enter/Space activate it, arrows walk its group (list rows, menu items, radios, segments) or step its value (sliders, split dividers), Home/End jump to the group edges.
- **Quiet focus** is the bookkeeping register: a pointer press (or programmatic focus — `autofocus`, the automation `focus` action) records which widget the user last touched, with no ring drawn. It exists so activation can find "the thing the user last touched", not to promise a keyboard cursor. Editable text kinds are the exception and show their ring and caret however focus arrives — a caret is a visible promise.

A key event then resolves top to bottom — the focused widget always outranks the app:

1. **Runtime focus motion.** Tab moves ring focus; group arrows and Home/End walk the focused widget's focus group (menu items, radios, segments under either register — list rows only under the ring, the quiet-list-row rule below), and where selection follows focus (tree rows), the landing row's select dispatches. These keys are consumed by the move.
2. **The focused widget's bound handler.** Activation keys resolve to the widget's `on-press`/`on-toggle` handler, Enter to a bound submit handler on text entry and list rows, arrow steps to a slider's `on-change` — Space on a ring-focused track row selects THAT row.
3. **Structural consume.** A focused widget that answers the key structurally consumes it silently even with nothing bound: any key on an editable text widget (typing must stay typing — checked by widget kind, so a search field blocks app shortcuts without knowing they exist), and any key the widget's kind maps as a control intent.
4. **The app-level fallback.** Only an unclaimed key-down reaches `Options.on_key` (a target-less event — nothing focused — skips straight here). The fallback is a plain function from the key event to an optional Msg:

```zig
// options: .on_key = onKey,
pub fn onKey(keyboard: canvas.WidgetKeyboardEvent) ?Msg {
    if (keyboard.modifiers.hasNavigationModifier() or keyboard.modifiers.shift) return null;
    if (std.ascii.eqlIgnoreCase(keyboard.key, "space")) return .toggle_play;
    if (std.ascii.eqlIgnoreCase(keyboard.key, "arrowdown")) return .select_next;
    if (std.ascii.eqlIgnoreCase(keyboard.key, "arrowup")) return .select_previous;
    if (std.ascii.eqlIgnoreCase(keyboard.key, "enter")) return .play_selected;
    return null;
}
```

One rule bridges the registers: **quiet focus on a plain list row routes no keys.** A quietly focused `list-item` (what clicking a row leaves behind) is transparent to the keyboard — arrows neither walk the row group nor escalate to the ring, Enter and Space fire nothing on the row, and the key reaches the app target-less, exactly as if nothing were focused. This supports selection-follows-intent apps: in a music library the selection is MODEL state — a click selects a row, and the arrows should keep moving that app-owned selection through the fallback (`select_next`/`select_previous` above), not silently start walking engine focus from whichever row was clicked last; without the rule, the first click anywhere in a list re-aimed every arrow and swallowed the spacebar. Tab restores the full contract — a ring-focused row walks on arrows, selects on Space, submits on Enter — and rows carrying `role="treeitem"` are exempt entirely: a [tree](/docs/components/tree)'s roving keymap is the feature, under either register. Every other widget kind keeps its keys under quiet focus too (a quietly focused button still presses on Space); the rule is scoped to plain list rows because only there quiet focus is bookkeeping rather than a keyboard cursor.

`examples/soundboard` is the worked example, pinned end-to-end in its tests: Space toggles the transport from anywhere — after clicking a track row (quiet, transparent), from a focused slider or scroll region (space is not one of their keys, so it falls through) — while a tabbed-to row takes Space for select and Enter for play, and the search field takes every key as typing. Automation note: `native automate widget-action <view> <id> press` escalates a plain list row to the ring register before dispatching its synthesized activation key, so a scripted activation reaches the row the way Tab-then-Enter would.

## Trackpad pinch

Pinch gestures use the view-global `on_pinch` channel, outside widget hit testing. Events have phases `begin`, `change`, and `end`; host cancellation maps to `end`. Each change carries an incremental `scale` delta: apply `zoom *= 1 + scale`, rather than summing deltas. Coordinates `x`/`y` are view-local pointer positions. `window_id` and view `label` identify the source; TypeScript uses `windowId` and `label`.

```zig
// options: .on_pinch = onPinch,
pub fn onPinch(pinch: platform.PinchEvent) ?Msg {
    return switch (pinch.phase) {
        .change => .{ .zoom = .{ .factor = 1 + pinch.scale, .cx = pinch.x, .cy = pinch.y } },
        .begin, .end => null,
    };
}
```

TypeScript cores export the same channel as `pinchMsg` (the `keyMsg` shape), with the record types importable from `@native-sdk/core/events`:

```ts
import { type PinchEvent } from "@native-sdk/core/events";

export function pinchMsg(pinch: PinchEvent): Msg | null {
  if (pinch.phase !== "change") return null;
  return { kind: "zoomed", factor: 1 + pinch.scale, cx: pinch.x, cy: pinch.y };
}
```

macOS emits pinch from trackpad magnification events. Windows and Linux do not currently emit the channel. Recorded input replays the same events. Tests can use `native automate widget-pinch <view-label> <scale> [x y]`, where scale is the final multiplicative zoom and the default anchor is the view center.

## File drops

Native file drops reach a Zig `UiApp` through `Options.on_drop`, after any targeted canvas-widget drop handler. The event carries the source `window_id`, `view_label`, an optional view-local `point`, and every dropped path. TypeScript cores export the same channel as `dropMsg`; import its structural record from `@native-sdk/core/events`. Paths are `Uint8Array` byte text so non-ASCII filesystem names cross without a JS string conversion:

```ts
import { type FileDropEvent } from "@native-sdk/core/events";

export function dropMsg(drop: FileDropEvent): Msg | null {
  if (drop.paths.length === 0) return null;
  return { kind: "file_opened", path: drop.paths[0] };
}
```

The platform event is journaled before either route, so record/replay delivers the identical source, point, and path bytes. The macOS system host reports labeled, view-local points for canvas and WebView drops; an unlabeled window region keeps an empty `viewLabel` and reports a window-content point. A host that cannot resolve either leaves `viewLabel` empty and `point` null.

## Native scrolling and context menus

On macOS, every non-virtualized `scroll` region is driven by an invisible `NSScrollView`: momentum and the system overlay scrollbar are OS-computed while the engine renders the content. This needs no app code — scroll offsets stay on the widget (`sync`, snapshot offsets, and programmatic scrolls work unchanged), and the engine's drawn scrollbar stands down for natively driven regions. Other platforms keep the engine's wheel physics.

Scroll regions pin at their content edges by default on every path — no rubber-band bounce; kinetic motion stops cleanly at the boundary. Bouncing is a per-region opt-in: `overscroll="rubber_band"` in markup or `ElementOptions.overscroll = .rubber_band` in Zig views (the native macOS scroller gets elastic edges, the engine physics overscroll under resistance and spring back). The `ScrollPhysics.overscroll` design token flips the app-wide default; per-region values override it, and `overscroll="none"` pins a region regardless of the token.

Right/ctrl-click presents a real OS context menu at the pointer: `NSMenu` on macOS, `TrackPopupMenu` on Windows, `GtkPopoverMenu` on Linux. Authors declare ONE menu and the platform decides presentation: hosts without a native menu presenter (the mobile toolkit hosts and embed hosts today) mount the same declared items as an anchored canvas surface at the click point automatically — never two authored menus, never a canvas imitation where the OS menu exists. In markup, the menu is a `<context-menu>` child of the interactive element it answers — a hit-target kind or an element made interactive by `on-press`, `on-double-press`, `on-toggle`, `on-hold`, or `on-drag` — holding `menu-item`s (`on-press` required, `disabled` optional) and `separator`s, with `if`/`else`/`for` to swap or repeat items:

```html
<list-item on-press="open:{entry.id}" label="{entry.title}">
  <text grow="1">{entry.title}</text>
  <context-menu>
    <menu-item on-press="open:{entry.id}">Open</menu-item>
    <separator />
    <menu-item on-press="delete:{entry.id}">Delete</menu-item>
  </context-menu>
</list-item>
```

The Zig builder's mirror is `ElementOptions.context_menu`:

```zig
ui.listItem(.{
    .on_press = Msg{ .select = entry.index },
    .context_menu = &.{
        .{ .label = "Open", .msg = Msg{ .select = entry.index } },
        .{ .separator = true },
        .{ .label = "Delete", .msg = Msg{ .delete = entry.index } },
    },
}, entry.title)
```

Editable text fields present the standard Cut / Copy / Paste / Select All menu with no declaration, wired to the same clipboard actions as the keyboard shortcuts; a selected static text presents Copy. Tests drive the native path without pixels: snapshots list each widget's declared items (`context_menu=["Open","Delete"]`), and `native automate widget-context-menu <view> <id> <item-index>` invokes an item through the same dispatch a real pick takes.

## Effects

For anything asynchronous — running `gh`, streaming an agent's stdout — `update` never performs the work; it describes it, and results come back as ordinary Msgs. In a TypeScript core the description is `Cmd` data returned beside the next model ([TypeScript Cores: Effects](/docs/typescript#effects-are-cmd-data) has the whole vocabulary); in a Zig core, `update` takes a third parameter, the effects channel — declare `.update_fx` instead of `.update` (two-argument apps are unchanged). The subprocess stream in both:

```ts
case "start":
  return [model, Cmd.spawn(
    [asciiBytes("gh"), asciiBytes("issue"), asciiBytes("list")],
    {
      key: "issues",
      line: "line",   // each stdout line dispatches { kind: "line", text: Uint8Array }
      exit: "exited", // a clean exit dispatches { kind: "exited", code: number }
      err: "stream_failed",
    },
  )];
case "cancel":
  return [model, Cmd.cancel("issues")]; // the stream's err arm gets `cancelled`
case "line":
  return { ...model, lines: [...model.lines, { text: msg.text }] };
```

```zig
const Effects = native_sdk.UiApp(Model, Msg).Effects;

pub fn update(model: *Model, msg: Msg, fx: *Effects) void {
    switch (msg) {
        .start => fx.spawn(.{
            .key = stream_key,                    // caller-chosen u64, kept in the model
            .argv = &.{ "gh", "issue", "list" },
            .on_line = Effects.lineMsg(.line),    // each stdout line becomes Msg{ .line = ... }
            .on_exit = Effects.exitMsg(.exited),  // the exit becomes Msg{ .exited = ... }
        }),
        .cancel => fx.cancel(stream_key),
        .line => |line| model.recordLine(line),   // copy the bytes — the slice dies with this call
        .exited => |exit| model.finish(exit),
    }
}
```

The process runs on a worker thread the runtime owns; results post to a bounded queue, the platform loop is nudged, and Msgs dispatch on the loop thread — the model is never touched off-thread. Views never spawn: a button dispatches a Msg, and that Msg's update arm spawns.

Boot-time effects go in `.init_fx`, the app's init command: it runs exactly once on the installing frame, before the first view build, so a loading flag set there is in the very first paint and the boot fetch is already in flight when the window appears. Results arrive as ordinary Msgs; it works with either update form.

Line streaming truncates single-line JSON at the 4 KiB line cap, so tools like `gh --json` use collect mode instead: spawn with `.output = .collect` and whole stdout (up to 512 KiB) arrives once on the exit Msg as `exit.output`, together with the child's stderr tail (last 4 KiB) in `exit.stderr_tail` — check it when `exit.code != 0`. No line Msgs fire for a collect spawn, overflow arrives cut with `output_truncated`/`stderr_truncated` set (never silently), and both slices die with the update call — copy what the model keeps.

HTTP requests ride the same channel: one buffered request on a worker thread, delivering its terminal outcome as exactly one Msg — a real response (any status, non-2xx included), a classified failure (`rejected`, `connect_failed`, `tls_failed`, `protocol_failed`, `timed_out`), or `cancelled`:

```ts
case "load":
  return [model, Cmd.fetch(
    { url: asciiBytes("https://api.example.com/search?q=zig") },
    { key: "search", ok: "fetched", err: "fetch_failed" },
  )];
case "stop":
  return [model, Cmd.cancel("search")];
case "fetched":
  // the ok arm carries { status: number, body: Uint8Array } — a 404 is
  // still ok: an HTTP-level error is a delivered response
  return { ...model, status: msg.status, body: msg.body };
```

```zig
.load => fx.fetch(.{
    .key = search_key,                                 // fetches share the spawn slots and key space
    .url = "https://api.example.com/search?q=zig",
    .on_response = Effects.responseMsg(.fetched),      // Msg{ .fetched = native_sdk.EffectResponse }
}),
.stop => fx.cancel(search_key),
.fetched => |response| model.recordResults(response),  // copy response.body — the slice dies with this call
```

Response bodies are binary-safe and bounded (256 KiB; longer arrives cut with `truncated = true`), the whole exchange honors a per-fetch timeout (default 30 s), and cancelling a fetch delivers exactly one `cancelled` Msg with nothing after it.

Files ride the same channel without smuggling an `Io` handle from `main` into `update`. Whole-file reads and writes remain bounded at 1 MiB; `appendFile`, `statFile`, and `deleteFile` handle logs and file lifecycle, while the dedicated streaming family carries large imports and atomic exports. External paths require the `filesystem` permission; app-owned directories are exempt after symlink-safe normalization. See [Files & Streaming](/docs/files).

```zig
.save => fx.writeFile(.{
    .key = save_key,
    .path = model.sessionPath(),
    .bytes = model.snapshotJson(),                 // copied at call time
    .on_result = Effects.fileMsg(.saved),          // Msg{ .saved = native_sdk.EffectFileResult }
}),
.saved => |result| model.noteSaved(result.outcome),
```

Desktop notifications use the same bounded platform-services facade but are intentionally fire-and-forget: once the host accepts a request, the OS can still suppress it through Focus / Do Not Disturb or the user's notification settings, so a success Msg would over-promise. A nonempty `id` replaces an earlier notification with that id. Paired `actionLabel` / `actionCommand` fields add an action whose activation returns through the ordinary application command path while the process is running. Return `Cmd.showNotification` from a TypeScript core or call `fx.showNotification` from a Zig `update_fx` arm; invalid or over-bound fields, unpaired action fields, invalid command names, and unavailable services fail closed. The fake executor and session replay never display one; the null test platform records and can activate one deterministically.

```ts
case "build_finished":
  return [model, Cmd.showNotification({
    id: asciiBytes("build-status"),
    title: asciiBytes("Build finished"),
    subtitle: asciiBytes("native-sdk"),
    body: asciiBytes("All checks passed."),
    actionLabel: asciiBytes("Open results"),
    actionCommand: asciiBytes("build.open-results"),
  })];
```

```zig
.build_finished => fx.showNotification(.{
    .id = "build-status",
    .title = "Build finished",
    .subtitle = "native-sdk",
    .body = "All checks passed.",
    .action_label = "Open results",
    .action_command = "build.open-results",
}),
```

Failure and overflow are always visible: a spawn that cannot run delivers an exit Msg with reason `rejected`, a fetch that cannot run delivers a response Msg with outcome `rejected`, and a file effect that cannot run delivers a result Msg with outcome `rejected`; dropped or truncated lines carry counts and flags; `cancel` kills and reaps the process and always ends in exactly one `cancelled` exit Msg, with no further line Msgs after it. Tests use the fake executor (`effects.executor = .fake`) to assert on spawn, fetch, and file requests and feed synthetic lines, stderr (`feedStderr`, collect spawns), exits, responses, and file results back deterministically — set it before the first frame and `init_fx` boot spawns are recorded too. See `examples/effects-probe`.

For timestamps, the facade owns the clocks (Zig 0.16 puts `std.time` behind `std.Io`, which `update` never sees): `native_sdk.nowMs()` / `nowNanoseconds()` read the wall clock and `monotonicMs()` / `monotonicNanoseconds()` the duration clock. Time-dependent logic stores the `native_sdk.Clock` seam in the model (`.system` by default) so tests substitute a deterministic `native_sdk.TestClock` and advance it by hand.

## Secondary windows

In a default TypeScript app, export `windows(model): readonly WindowDescriptor[]` and put each possible window's markup at `src/windows/<label>.native`; the generated launcher compiles those files, maps descriptor labels to them, and includes shared components they import from nested paths under `src/windows/`. The main `src/app.native` view can import components anywhere under `src/`. Construct descriptors with `windowDescriptor`. `restorePolicy: "center_on_primary"` centers a fresh descriptor with no `x`/`y` on macOS; Windows and Linux currently keep their native default placement. The default policy is `"clamp_to_visible_screen"`. `closePolicy: "quit"` routes `onCloseCommand` through `commandMsg`; `"hide"` retains the window and dispatches no close command. `titlebar` includes `"chromeless"` for fully skinned windows. `examples/system-monitor` is the complete default-path reference.

In Zig cores and custom wiring, windows are model-declared exactly like the tray: `Options.windows_fn` returns the window descriptors that should exist right now (presence IS liveness), and `Options.window_view` builds each declared window's whole canvas tree, keyed by the descriptor's window label. After every dispatched Msg the runtime reconciles: windows the model started declaring are created (a source-less native window wearing one `gpu_surface` view with the descriptor's `canvas_label`, inheriting the main canvas's gpu options), windows it stopped declaring close, and every open window's view rebuilds from the same model — a theme picked in the settings window restyles the main window on the same dispatch. There is no `visible` flag: transient visibility is host state changed through `hideWindow`/`showWindow` or a `.hide` close policy; stop declaring a window to really close it and release its retained views.

```zig
fn windows(model: *const Model, scratch: *App.WindowsScratch) []const App.WindowDescriptor {
    var count: usize = 0;
    if (model.settings_open) {
        scratch.windows[count] = .{
            .label = "settings",
            .canvas_label = "settings-canvas",
            .title = "Settings",
            .width = 360,
            .height = 320,
            .min_width = 320,               // the WINDOW enforces the floor (macOS contentMinSize)
            .min_height = 280,              // — resizes stop instead of clamping panes
            .on_close = .settings_closed,   // the user's close button, as a Msg
        };
        count += 1;
    }
    return scratch.windows[0..count];
}

fn windowView(ui: *App.Ui, model: *const Model, window_label: []const u8) App.Ui.Node {
    _ = window_label; // one declared window; multi-window apps switch on it
    return settingsPane(ui, model);
}

// options: .windows_fn = windows, .window_view = windowView,
```

Input from any window dispatches Msgs through that window's own tree with its window identity. `WindowDescriptor.close_policy` accepts `.quit` (the default) or `.hide`, with the same behavior and platform safeguards as manifest windows. A USER close under `.quit` dispatches the descriptor's `on_close` Msg — the dismissal precedent applied to windows: the window is already gone as an optimistic echo, the model clears its open flag in `update` (or keeps declaring the window and the next rebuild brings it back — source wins). Under `.hide`, the window stays alive with its views, `on_close` does not fire, and `showWindow(label)` brings it back. A close the model itself initiated (it stopped declaring the window) never echoes a Msg. At most `UiApp.max_ui_windows` (4) secondary windows may be declared; the excess warns and is ignored. `min_width`/`min_height` declare a content min-size floor the window itself enforces (macOS `contentMinSize`): the user's resize stops at the floor instead of the layout clamping and clipping panes below their declared minimums — the same fields exist on app.zon windows and `ShellWindow`, and the startup window applies its declaration at the host create like `titlebar`. 0 (the default) leaves the axis at the platform's own minimum.

Markup deliberately binds ONE window's content: there is no `window` element in the closed grammar, because windows are app-shell concerns, not view-tree concerns. A markup-authored secondary window is a `canvas.CompiledMarkupView` whose `build` the `window_view` fn calls for the matching label.

Automation reaches every window with the same verbs: snapshots enumerate all windows and their widgets, and `widget-click`/`widget-hold`/`widget-key`/`screenshot` address a canvas by its view label across all open windows — a settings window's canvas is as drivable as the main one. See `examples/system-monitor` (the gear chip opens a model-declared settings window).

## Hidden titlebar: drag regions and chrome insets

Set `titlebar` to `"hidden_inset"` to extend content under the titlebar while retaining OS window buttons. This is available on macOS, Windows, and Linux. The startup window declaration applies from the first frame. The following channels let an app header reserve space for window controls:

1. **`window-drag="true"`** (builder: `.window_drag = true`) marks an element as a window-drag surface. A press on its background — or on plain text/icons inside it — moves the window (the drag starts only on actual movement; a plain click moves nothing), and a double-click applies the OS titlebar convention (macOS: zoom, honoring the user's titlebar double-click preference). Press fall-through keeps working: a button inside the drag header stays a button, because the press walk claims interactive children before the drag surface.
2. **`on_chrome`** delivers overlay insets and the OS button frame before the first view build and whenever geometry changes. macOS reserves the leading traffic-light area; Windows and Linux report their native control placement. Use these measurements instead of hardcoded padding. Standard-chrome and fullscreen windows report no titlebar overlay. The hook currently covers the main canvas window; TypeScript cores use `chromeMsg` from `@native-sdk/core/events`.
3. The header itself is ordinary markup:

```html
<row padding="10" cross="center" background="surface" window-drag="true" label="Toolbar">
  <spacer width="{chrome_leading}" />   <!-- clears the traffic lights; 0 in fullscreen -->
  <text>My Document</text>
  <spacer grow="1" />
  <button size="sm" on-press="save">Save</button>  <!-- stays clickable -->
</row>
```

`titlebar = "hidden_inset_tall"` is the same shape with the taller unified band: macOS centers the traffic lights in a 52pt band (the Notes/Linear look) instead of the compact one — pick it when the header row is toolbar-height so the lights sit on its centerline. Fullscreen falls back cleanly in both variants.

Windows implements hidden titlebars with DWM non-client handling, and Linux uses GTK client-side decorations. Both report control insets and support drag regions. Mobile hosts use the same chrome channel for safe-area insets; subscribing transfers responsibility for that padding to the app. See [Platform Support](/docs/platform-support) and `examples/markdown-viewer`.

`titlebar = "chromeless"` removes ALL OS chrome — the titleband AND the system buttons, on every desktop (macOS borderless, Windows caption-less popup, Linux undecorated). It is an EXPLICIT opt-in for fully-skinned apps whose chassis draws its own working close/minimize controls wired to the real window-action effects (`fx.closeWindow(label)` / `fx.minimizeWindow(label)`); ordinary apps should keep the hidden styles above, which keep the real OS buttons. Drag regions keep working; chrome insets are zero. See `examples/deck` for the full shape.

## Images

Images are runtime-registered resources. The app chooses an `ImageId` (`u64`; 0 means no image), registers decoded straight-alpha RGBA8 pixels, and binds that id on `image` or `avatar`. Encoded images use platform decoders: CGImageSource on macOS/iOS, gdk-pixbuf on Linux, WIC on Windows, and BitmapFactory on Android. An embed host without an image service reports `error.UnsupportedService`.

Use `fx.loadImage` or `Cmd.imageLoad` to resolve a local or URL source, cache it, decode it, and register it. The result arrives as a message. [Dynamic Images](/docs/dynamic-images) covers cancellation, cache limits, and offline replay. Direct registration is also available when the app already owns encoded bytes or pixels:

```zig
.load => fx.fetch(.{ .key = avatar_key, .url = avatar_url, .on_response = Effects.responseMsg(.fetched) }),
.fetched => |response| {
    if (response.outcome == .ok and response.status == 200) {
        _ = fx.registerImageBytes(avatar_image_id, response.body) catch return;
        model.avatar_image = avatar_image_id;   // only a registered id reaches the view
    }
},
```

```zig
// in a Zig view:
ui.avatar(.{ .image = model.avatar_image, .semantics = .{ .label = "Octocat" } }, "OC"),
ui.image(.{ .image = model.chart_image, .width = 120, .height = 80, .semantics = .{ .label = "Chart" } }),
```

```html
<!-- in markup, image leaves and avatars bind the same model id;
     0 draws nothing / renders the initials -->
<image image="{chart_image}" width="120" height="80" label="Chart" />
<avatar image="{avatar_image}" label="Octocat">OC</avatar>
```

One registered image can be a texture atlas. `ElementOptions.image_src` is an optional `geometry.RectF` in decoded-image pixel coordinates; Native markup spells the same rectangle with all four source attributes. The widget frame stays the destination, a source rectangle crossing the registered image bounds is clipped, and cropped widgets use nearest sampling so adjacent atlas regions cannot bleed into the tile:

```zig
ui.image(.{
    .image = model.atlas_image,
    .image_src = geometry.RectF.init(64, 32, 24, 24),
    .width = 48,
    .height = 48,
    .semantics = .{ .label = "Warning badge" },
})
```

```html
<image image="{atlas_image}" source-x="64" source-y="32"
  source-width="24" source-height="24" width="48" height="48"
  label="Warning badge" />
```

`fx.registerImage(id, width, height, rgba8)` registers already-decoded pixels (the runtime copies them; your buffer is free on return), `fx.registerImageBytes(id, bytes)` decodes through the platform codec first, and `fx.unregisterImage(id)` frees the slot. Re-registering an id replaces its pixels and every view repaints — GPU caches re-upload off the changed content fingerprint, no invalidation calls. For caches, mint fresh ids (effect-key style, monotonically increasing) and unregister the evictee — never re-key different content onto a live id. Outside `UiApp`, the same registry is `Runtime.registerCanvasImage`/`registerCanvasImageBytes`/`unregisterCanvasImage`.

Capacities are fixed and loud (`canvas_limits`): 16 slots with a 1 MiB decoded-pixel target by default. Encoded photos decode aspect-preservingly to fit, while raw `fx.registerImage` pixels remain strict. Image-centric apps may raise the startup-frozen target through app.zon `.images.max_image_pixel_bytes`, up to 8 MiB; storage is lazy per used slot, but filling all 16 ceiling-sized slots is a declared 128 MiB high-water. Every encoded entry point, including direct `fx.registerImageBytes`, shares the flat 8 MiB source bound. `error.ImageRegistryFull`, `error.ImageTooLarge` (encoded source, raw pixels, or a codec-contract violation), `error.ImageDecodeFailed`, and `error.UnsupportedService` are never silent. Registered images render everywhere the canvas does: live presentation, screenshots, and automation. A missing id simply draws its fallback. In tests, the null platform's deterministic strict-PNG decoder also pins exact decode-to-fit dimensions and pixels.

## Rich text: inline spans and markdown

Mixed-style text inside one wrapped paragraph is markup's `<span>`: inline children of a `<text>` element, each styling one run with `weight` (`regular`, `medium` — the semibold rung — or `bold`), `mono`, `italic`, `scale` (a positive multiplier on the paragraph's base size), `underline`, and `foreground` (a color token name). `{bindings}` interpolate inside spans like any text, and the whole paragraph word-wraps as one block:

```html
<text>
  Disk <span weight="bold">{diskUsed}</span> of
  <span foreground="text_muted">{diskTotal}</span> — run
  <span mono="true">native doctor</span> if this looks wrong.
</text>
```

Whitespace between runs collapses to a single space; runs written with no whitespace between them abut, which is how a mono run takes trailing punctuation (`<span mono="true">init.zig</span>.`). Spans do not nest and take no events or keys — layout, identity, and the accessible name stay on the enclosing `<text>` — and the paragraph announces to assistive tech as **one** text run: spans are visual, not structural. Because a span paragraph always word-wraps (reserving its real wrapped height), the single-line `wrap`/`overflow` policies are rejected on it. Both engines lower spans through the same paragraph builder, so a markup span paragraph renders identically to a Zig-built one.

`scale` composes with the paragraph's typography: the run draws (and measures — line breaking is size-aware) at the base size times the multiplier, so `<text size="heading">` with a `scale="1.5"` span gives heading × 1.5, and the multiplier tracks the themable token steps instead of hard-coding a pixel size. One limit: a paragraph reserves ONE uniform line height sized by its largest scale, and every run sits on that line's shared baseline — mixed-scale paragraphs are for inline headings and hero stats next to their captions, not for stacking independent text sizes (stack separate `<text>` elements for that). Only positive finite multipliers are accepted; zero, negative, and non-numeric scales are teaching errors instead of runs silently drawn at the base size. `underline` is a pure decoration — it does not make the run a link.

The full span model — including `strikethrough`, `background` highlights, and link spans — is available from the Zig builder as *spans*:

```zig
const spans = [_]canvas.TextSpan{
    .{ .text = "Run " },
    .{ .text = "zig build test", .monospace = true },
    .{ .text = " then read " },
    .{ .text = "the guide", .link = "https://example.com/guide" },
};
ui.paragraph(.{ .on_link = Ui.linkMsg(.open_url) }, &spans)
```

Wrapping and measurement are span-aware (the platform text provider measures every run with the font it draws with), stacked paragraphs reserve their real wrapped height, and link spans are interactive: they carry `role=link` semantics in automation snapshots, show the pointing-hand cursor (the only place the engine uses it — controls keep the platform arrow, following native convention rather than the web's), and clicking one dispatches your `Msg` with the link payload.

Source presentation is the reusable `ui.code` / `<code>` component. It is deliberately bare: no background, border, radius, shadow, or padding. Wrap it in a `panel`, `card`, or another container when the design needs surface chrome. It wraps by default; `wrap="false"` preserves logical lines inside one horizontal scroll region, and `line-numbers` opts into a muted logical-line gutter. A definite `height` keeps overflow reachable with vertical scrolling (or both axes when wrapping is off):

```html
<code source="{component_source}" language="tsx" line-numbers wrap="false" width="480" label="Accordion example" />
```

```zig
ui.code(.{
    .language = .html,
    .line_numbers = true,
    .wrap = false,
    .width = 480,
}, model.component_source)
```

For example, an inset code well is ordinary composition:

```html
<panel padding="12">
  <code source="{component_source}" language="tsx" />
</panel>
```

Recognized languages receive theme-aware syntax color for keywords, types/numbers, strings, and comments: Zig; JavaScript/TypeScript; JSX/TSX; JSON; YAML; shell; Python; Rust; C, C++, C#, Java, Kotlin, and Swift; Go; HTML/XML/SVG; CSS/SCSS/Less; and SQL. HTML-family highlighting distinguishes element and component tags, attributes, strings, comments, and JavaScript/TypeScript expressions. Unknown or omitted tags stay plain monospace, and the bounded highlighting fallback never drops code. See [Code](/docs/components/code).

Markdown builds on the same model. `native_sdk.markdown` maps a GitHub-flavored subset — headings, inline styles, links (including bare `http(s)://` URLs, which autolink with trailing punctuation trimmed), bullet/ordered/task lists, fenced code blocks, blockquotes, rules, pipe tables, and `<details>` collapsibles — onto ordinary widgets:

```zig
const Md = native_sdk.markdown.Markdown(Msg);
Md.view(ui, issue.body, .{
    .images = model.markdown_images, // successful fx.loadImage mappings
    .on_link = Ui.linkMsg(.open_url),
    .on_details = Md.detailsMsg(.toggle_details),
    .details_expanded = &model.details_expanded, // caller-owned flags, elm-style
    .issue_link_base = "ghissue://", // opt-in: "#123" links to ghissue://123
})
```

Malformed input degrades to plain text — the build fn never fails. Task-list checkboxes render as disabled (display-only) checkboxes, and `<details>` expansion is state the caller's model owns.

Fenced code preserves every source indentation level and lowers through the same `ui.code` component, including JSX/TSX-aware HTML-family highlighting. Markdown keeps the component defaults: wrapping on, line numbers off.

GFM pipe tables map onto the real `table`/`table-row`/`table-cell` widgets: the header row renders bold, the delimiter row's `:---`/`:--:`/`---:` cells set per-column start/center/end alignment, every cell runs the full inline grammar (code, bold, links — links in cells are clickable), `\|` puts a literal pipe in a cell, and cells wrap at their column width (columns share the width equally in v1). A pipe block whose delimiter row is missing or mismatched is not a table and renders as plain paragraphs.

In markup, the `<markdown>` element wires all of this declaratively:

```html
<markdown source="{issue_body}" images="{markdownImages}" on-link="open_url" on-details="toggle_details" details-expanded="{details_expanded}" issue-link-base="ghissue://" />
```

`source` (required) is one `{binding}` producing the markdown text — a string field, zero-arg method, or arena-taking method. `images` optionally binds `[]const canvas.markdown.ResolvedImage`: give `canvas.markdown.collectImageSources` a caller-owned `[]canvas.markdown.CollectedImageSource`, consume each canonical source through `value()` while that storage is alive, load it through `fx.loadImage`, and retain the source, successful id, and dimensions in the model. Return those mappings from a model or arena-taking method; the view performs no I/O. `on-link` and `on-details` are bare `Msg` tags (the runtime supplies their payloads: the URL as `[]const u8`, the details index as `usize`), `details-expanded` names a `[]const bool` iterable through the same sources `for each` accepts, and `issue-link-base` (a literal prefix or one `{binding}`) turns `#123` references into links to base ++ number. Everything but `source` is optional — without the details wiring, `<details>` blocks render collapsed and inert. Both the interpreter and the comptime compiler implement the element identically.

## Pipeline components: stepper, timeline, and nav

Pipeline UIs (agent runs, wizards, activity feeds) get three composites. All three are ordinary compositions of existing widgets — same layout, theming, semantics, and hit-testing as hand-written trees.

A **stepper** is a horizontal stage track (the house stepper conventions: indicator + title per step, separators as connectors). The model owns the active index; earlier steps render completed (a check badge), later ones pending:

```html
<stepper active="{stage_index}">
  <step>Work</step>
  <step>Triage</step>
  <step>Review · {review_round}</step>
  <step>Fix</step>
  <step>Ready</step>
</stepper>
```

From Zig: `ui.stepper(.{ .active = model.stage_index }, &.{ .{ .label = "Work" }, ... })`. Semantics: a `list` of `listitem`s, the active step `selected`, each label carrying its state (`"Review (active)"`) plus its position, so automation can read the pipeline at a glance.

A **timeline** is a ledger list (the house timeline conventions: indicator + separator rail + title/description/meta per item). Items are leaves; the attributes carry the content:

```html
<timeline gap="4">
  <for each="ledger" key="slot" as="entry">
    <timeline-item title="{entry.title}" description="{entry.summary}" meta="{entry.meta}"
                   variant="{entry.tone}" on-press="open_step:{entry.slot}" />
  </for>
  <timeline-item title="Ready for review" indicator="OK" variant="secondary" connector="false" />
</timeline>
```

The leading badge is a small dot colored by `variant` (map run outcomes: `primary` for done, `destructive` for errors, `outline` for stopped, ...) or carries `indicator` text ("OK"); `connector="false"` ends the rail on the last item. With `on-press`, the item grows a trailing chevron and the press binds to the item's root, focusable with role `listitem` — clicks on the title/description/meta fall through to it, so a click anywhere on the item dispatches the message (dragging still selects the text). There is no built-in hover fill or line-clamping on the description in v1.

A **nav** container is the within-pane navigation stack (ledger item → step transcript, back button pops). Zig-only for now — markup swaps views with `<if>`:

```zig
ui.nav(.{ .active = model.nav_depth, .retain = true }, .{
    ledgerPane(ui, model),
    transcriptPane(ui, model),
})
```

The model owns the stack (push = set `nav_depth = 1`, pop = back to 0; out-of-range clamps). Pages are index-keyed, so a page's widget ids — and with them engine-owned scroll offsets and text edits — are stable across swaps. `retain = true` keeps inactive pages mounted but hidden: they keep engine state and drop out of rendering, hit-testing, focus traversal, and semantics; the default unmounts them (retained state is dropped). v1 swaps instantly — there is no push/pop animation yet — and focus does not transfer automatically: move it from `update` when the focused widget lives on the outgoing page.

## Charts

The `<chart>` element is the data-visualization composite: `<series>` children bind model iterables of `f32` and draw through the vector path pipeline with token colors, so charts re-theme with the palette, repaint exactly when their data changes, and report their series in automation snapshots. Three series kinds cover time-series dashboards:

```html
<chart grow="1" height="220" y-min="0" grid-lines="3" label="Star history">
  <series kind="area" values="{sdkStars}" color="accent" label="native-sdk" />
  <series kind="line" values="{examplesStars}" color="info" label="examples" />
</chart>
```

- **`line`** — a polyline through the values. A single sample renders as a dot.
- **`area`** — a line with a translucent fill down to the baseline (the builder's `fill = true` spelling).
- **`bar`** — one bar per value, always anchored at zero (the auto domain forces 0 in; negative values hang below the zero line). A zero value draws nothing — zero looks like zero.

Series `values` take one `{binding}` naming a `[]const f32` iterable — a model field, public declaration, or function (arena functions work), the same resolution set as `for each`, slice-valued template args included. Values are y samples at uniform x steps, oldest first; `NaN` marks a missing sample and draws a gap, so pad a filling window with leading `NaN` in a model function and the trace enters from the right. Colors are design-token references (`accent`, `info`, `success`, `warning`, `destructive`, ...), never raw colors, so charts hold up in both themes. The y domain derives from the data per side unless `y-min`/`y-max` pin it (literals or scalar bindings); `grid-lines` draws that many horizontal token hairlines (opt-in — none by default) and `baseline="true"` marks the zero line.

Axis tick labels are opt-in and draw in the muted text register inside reserved gutters. `x-labels="{binding}"` names a model iterable of strings — one category label per sample, oldest first — thinned deterministically to every Nth so labels never collide (and dropped entirely when a series downsamples: bucketed indices no longer name the labeled samples). `y-labels="true"` adds numeric ticks on a nice-step lattice (1/2/5 × 10^k, exact at their precision); with `grid-lines` set, the gridlines ride the same lattice, so the grid and its labels can never disagree.

`hover-details="true"` adds pointer-hover point details: the pointer snaps to the nearest sample, a hairline cursor marks it (line series get a dot on the hovered point), and a floating card shows the sample's label with every series' name and value. Interaction-only chrome — static renders never show it.

Series over 256 points use deterministic index-bucket min/max decimation to stay within the path budget while retaining extrema. This limit describes the rendering algorithm, rather than a measured performance result.

Semantics: role `chart`, a generated series summary as the label (`"chart: cpu 3 pts last 0.75"` — describing the source series, pre-decimation) unless you set one, and the first series' latest value as the accessibility value, so automation can assert on live data without pixel access. Charts never claim presses: clicks fall through to the nearest pressable ancestor, so a chart inside a pressable row keeps the row clickable (`hover-details` makes the chart a hover target only).

The series set is static — the plot's shape is part of the design and the DATA varies through the bindings. `ui.chart` (the same code the markup lowers through) is the escape hatch for what the grammar deliberately excludes: `.band` envelopes (a min/max band pairs a second lower-edge slice per point) and series sets composed dynamically at view time. See [Chart](/docs/components/chart) for the full attribute tables.

## Accessibility

Every interactive element needs an accessible name — the text a screen reader announces and automation snapshots address widgets by. Name sources are the element's text content, the `text` attribute, or `label="..."`; text-entry controls take `label` or `placeholder` (the accessibility bridges announce the placeholder as the fallback name). The sources never combine: an explicit `label` REPLACES the element's text as the accessible name — while a label is set, the visible text is neither announced nor targetable, so never label an element whose visible text your automation greps for.

This is a machine check, not a review item. An unnamed control, an icon-only control without a label (the icon name is a drawing instruction, not a label), or a misused role (`role="tree"` on an element that can never hold rows) is a validation error in `native markup check`, both view engines, and the LSP — the rubric is whether a screen reader user is fully blocked. Unnamed images and labels that duplicate the text they shadow degrade rather than block, so they report as warnings (`--strict` promotes them); the explicit `label=""` marks an image decorative on purpose.

Zig-built views get the same discipline at tree level: `canvas.expectA11yAuditSweepClean` lays out the real tree and reports interactive widgets announced with no name (including dynamic labels that resolve empty at runtime), focusable widgets keyboard traversal can never reach, and identically labeled sibling controls — adopt it in your test suite next to `canvas.expectLayoutAuditSweepClean`. Contrast checking and focus-visible styling checks are not part of the audit yet.

## Tooling

- `native markup check src/app.native` — instant validation with `file:line:column` errors, including the font-coverage tofu guard: literal text with a codepoint outside the bundled face is a teaching error naming the character (it renders as a tofu box on the reference/screenshot and mobile paths — [register a font](/docs/fonts) that covers it and bind the text from the model, or use a vector icon or plain words; the static check knows only the bundled face's coverage). Dynamic strings get the same lesson as a Debug-build diagnostic when the view builds. Accessibility findings ride the same pass: unnamed interactive controls and role misuse are errors, unnamed images and redundant labels are warnings (see [Accessibility](#accessibility)).
- `native markup lsp` — diagnostics, completion, and hover in your editor (see `editors/native-markup/` in the repo for VS Code, Helix, and Neovim setup).
- The automation harness drives widgets and reads accessibility snapshots and screenshots headlessly — see [Automation](/docs/automation).

### The model–view contract

`native check` verifies markup against your app's actual model surface, with no app compile in the loop:

- View → model: every binding path, `for each` iterable, `key` field, message tag, and payload type must exist on the model with the right shape, and expressions type-check with the real binding types. Zig cores also get a did-you-mean over the model's actual fields.
- Model → view: model fields, derived helpers/query functions, and `Msg` tags that no view binds or dispatches are reported as **warnings**. State only `update` or effects logic touches is legitimate, so opt it out in the core's vocabulary: TypeScript declares `export const viewUnbound = ["nextId", "tick"] as const` in `src/core.ts`; Zig declares `pub const view_unbound = .{ "next_id", "tick" };` on `Model` or `Msg`. Use the names exactly as that core authored them. `--strict` promotes the warnings to failures.
- Template args are part of a template's interface: use-site arguments flow into the template body, slot content checks in the consumer's scope, and the check follows the whole `<import>` closure.

**TypeScript core** (`src/core.ts`): `native check` compiles `src/app.native` and every `src/windows/*.native` view against the core's contract with the same view compiler the build uses, so a view that checks is a view that builds. There is no artifact to refresh, and the check is never stale.

**Zig core**: the check reads the model-contract artifact `zig-out/model-contract.zon`, a reflection of your `Model`/`Msg` that `native test` refreshes (apps that own their build can also run `zig build model-contract` directly). A missing or stale artifact (the artifact carries a hash of the app's core sources) degrades to structural checking with one loud line ("model contract: not yet built - bindings checked structurally only; run `native test` to enable typed checks") — never a false pass; the compiled engine still enforces the same contract at build time, and a conformance suite holds the two checkers to identical accept/reject sets. Model state consumed only by a Zig-built view needs `view_unbound` too — the markup checker cannot see Zig view reads.

For the component catalog and styling model, see [Built-in Components](/docs/built-in-components). For dropping down to the programmatic API, the `canvas.Ui(Msg)` builder produces exactly the same trees the markup compiles to.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Package Distribution
url: "https://native-sdk.dev/docs/packages"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The Native SDK is distributed as a Zig codebase plus npm packages for the CLI. The former primitive modules (`geometry`, `assets`, `app_dirs`, `trace`, `app_manifest`, `diagnostics`, and `platform_info`) are now internal Native SDK modules and are available through the main `native_sdk` import instead of standalone Zig packages.

## Install

```bash
npm install -g @native-sdk/cli
```

See the [Quick Start](/docs/quick-start) for the full flow.

## How the npm install works

`@native-sdk/cli` installs the `native` command. Platform-specific optional dependencies supply the prebuilt CLI binary. The package also declares the TypeScript checker and scriptc compiler dependencies used to build TypeScript apps.

The CLI package includes the SDK source and resolves it from its install location. Set `NATIVE_SDK_PATH` to build against a checkout. Offline builds require the compiler dependencies, Zig toolchain, and any app dependencies to be installed already.

The CLI can also download the pinned Zig toolchain: on first `native dev|build|test`, the CLI uses a compatible `zig` from your PATH or offers to download the pinned version into `~/.native/toolchains/` (checksum-verified).

All packages share one version, and the binary packages are pinned exactly, so a given `@native-sdk/cli` version always runs the binary built from the same commit.

## Packages

<table>
  <thead>
    <tr>
      <th>
        Package
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `@native-sdk/cli`
      </td>

      <td>
        Installs the

        `native`

        command, carries the SDK source apps build against, and selects the right binary package for your platform
      </td>
    </tr>

    <tr>
      <td>
        `@native-sdk/cli-darwin-arm64`
      </td>

      <td>
        Prebuilt

        `native`

        binary for macOS on Apple silicon
      </td>
    </tr>

    <tr>
      <td>
        `@native-sdk/cli-darwin-x64`
      </td>

      <td>
        Prebuilt

        `native`

        binary for macOS on Intel
      </td>
    </tr>

    <tr>
      <td>
        `@native-sdk/cli-linux-arm64-gnu`
      </td>

      <td>
        Prebuilt

        `native`

        binary for Linux arm64 (glibc)
      </td>
    </tr>

    <tr>
      <td>
        `@native-sdk/cli-linux-x64-gnu`
      </td>

      <td>
        Prebuilt

        `native`

        binary for Linux x64 (glibc)
      </td>
    </tr>

    <tr>
      <td>
        `@native-sdk/cli-linux-arm64-musl`
      </td>

      <td>
        Prebuilt

        `native`

        binary for Linux arm64 (musl)
      </td>
    </tr>

    <tr>
      <td>
        `@native-sdk/cli-linux-x64-musl`
      </td>

      <td>
        Prebuilt

        `native`

        binary for Linux x64 (musl)
      </td>
    </tr>

    <tr>
      <td>
        `@native-sdk/cli-win32-x64`
      </td>

      <td>
        Prebuilt

        `native`

        binary for Windows x64
      </td>
    </tr>

    <tr>
      <td>
        `@native-sdk/cli-win32-arm64`
      </td>

      <td>
        Prebuilt

        `native`

        binary for Windows on ARM
      </td>
    </tr>
  </tbody>
</table>

The wrapper scripts and packaging metadata live in `packages/native-sdk/` in the repository, and every binary is also published as a checksummed GitHub release asset for use outside npm.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Model Persistence
url: "https://native-sdk.dev/docs/persistence"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Model persistence stores the TypeScript core's committed `Model` without giving `update` filesystem access or making the app own a serialization loop. Declare the capability and an increasing schema version in `app.json`:

```json title="app.json"
{
  "capabilities": ["persist"],
  "persist": {
    "version": 1,
    "debounce_ms": 500,
    "restore": {
      "ok": "restored",
      "none": "fresh_boot",
      "err": "restore_failed"
    }
  }
}
```

The three route names refer to `Msg` arms. `ok` and `none` are void arms; `err` carries one `Uint8Array` field containing one closed reason: `corrupt`, `version_unknown`, `migrate_failed`, `io_failed`, or `rejected`.

```ts title="src/core.ts"
import { Cmd } from "@native-sdk/core";

export interface Model {
  readonly draft: Uint8Array;
  readonly saves: number;
}

export type Msg =
  | { readonly kind: "edited"; readonly draft: Uint8Array }
  | { readonly kind: "restored" }
  | { readonly kind: "fresh_boot" }
  | { readonly kind: "restore_failed"; readonly reason: Uint8Array };

export const viewUnbound = ["restored", "fresh_boot", "restore_failed"] as const;

export function initialModel(): Model {
  return { draft: new Uint8Array(0), saves: 0 };
}

export function update(model: Model, msg: Msg): Model | [Model, Cmd<Msg>] {
  switch (msg.kind) {
    case "edited":
      return [{ ...model, draft: msg.draft, saves: model.saves + 1 }, Cmd.persist()];
    case "restored":
    case "fresh_boot":
    case "restore_failed":
      return model;
  }
}
```

`Cmd.persist()` snapshots the model from that committed update. The command carries no model bytes; the compiled core exposes its generated canonical encoder to the host. The host coalesces requests on a trailing edge, performs filesystem work off the update thread, keeps at most one write in flight, and force-flushes the pending tail during backgrounding and graceful shutdown. A failed write dispatches the configured `err` route, just like a failed boot restore. The default debounce is 500 ms; `debounce_ms` accepts 0–60,000.

Snapshots live in the platform app-data directory as `snapshot.nsd`. Installation is atomic: the engine writes and syncs a temporary file, renames it over the primary, and keeps one structurally valid prior generation as `snapshot.nsd.bak`. A corrupt or torn primary falls back to that backup. The generated body is little-endian and uses tagged, length-delimited Model fields; snapshot bodies are bounded at 16 MiB, independently of the raw file-effect limits.

## Boot and replay

Before the first frame, the engine restores the canonical model and dispatches exactly one configured route. `restored` observes the restored model; `fresh_boot` means neither generation exists. Restore and migration failures leave the initial model in place and dispatch `restore_failed` with the reason bytes.

The restore result crosses the ordinary effect journal boundary. Recording stores non-empty snapshot bytes in the session blob store; replay feeds those bytes back without reading or writing the live app-data directory. `Cmd.persist()` remains on the replay command stream for fingerprint parity, but its host binding is a no-op.

## Schema versions and migration

Increase `.persist.version` whenever the `Model` shape changes, and never reuse a version. `native check` verifies that the configured `ok` and `none` routes name void `Msg` arms and `err` names a one-`Uint8Array`-field arm; `native dev --core` runs the same fence before starting its virtual host. Check also remembers the last accepted version/fingerprint pair under `.native/cache` and reports NS1068 when the shape changes without a bump or the version moves backward. The snapshot header carries that app version, the generated model-only shape fingerprint, and the compiler's snapshot-format version, so a cold checkout still fails closed at runtime: a same-version shape mismatch reports `corrupt`, and a snapshot from a future app version reports `version_unknown`. That older binary also refuses subsequent writes with `version_unknown`, preserving the newer snapshot across a rollback.

To accept an older version, export the pure migration hook from `src/core.ts`:

```typescript
export function migrate(snapshot: Uint8Array, fromVersion: number): Model {
  // Decode the versioned legacy bytes and construct the current Model.
  // Throw a subset value when the legacy bytes cannot be migrated.
  return decodeLegacyModel(snapshot, fromVersion);
}
```

The checker requires the exact `(Uint8Array, number) => Model` shape. A successful migration is encoded in the current format, installed as the new snapshot, restored before delivery, and journaled as an ordinary successful restore. A thrown value, missing hook, or invalid result reports `migrate_failed`.

The snapshot persists the whole model. Do not put tokens or passwords in it; declare the `credentials` capability and permission, store them with [`Cmd.credentials.set`](/docs/capabilities), and consume get-result bytes without retaining them in Model. Credential recording is redacted and replay receives only a deterministic same-length placeholder. Raw `Cmd.readFile` and `Cmd.writeFile` remain the escape hatch for user-visible files, exports, and blobs—not the default model store.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Platform Support
url: "https://native-sdk.dev/docs/platform-support"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Native SDK has desktop hosts for macOS, Linux, and Windows, plus experimental mobile support. This page lists the implementation and verification tier of each platform capability. Unsupported operations return runtime errors.

## Support Matrix

macOS, Linux, and Windows run full desktop apps through their own platform hosts. Mobile is experimental — the marked columns are verified on the simulator and emulator, but APIs and tooling there may still change, and desktop is the mature surface. iOS and Android canvas apps are host-tier: the toolkit owns the entire mobile application — `native dev --target ios|android` and `native package --target ios|android` build and generate the platform host from the app manifest, and your project carries zero host code. Embedding in a host app you own works on both mobile platforms and shares the same experimental status — see [Mobile](#mobile) below.

- First-class — implemented and exercised
- Works with caveats — real support, footnote applies
- Embed-level — runs inside a host app you own
- Not available today
- Experimental — verified on the simulator/emulator; APIs and tooling may still change

<table>
  <thead>
    <tr>
      <th>
        What you get
      </th>

      <th>
        macOS
      </th>

      <th>
        Windows
      </th>

      <th>
        Linux
      </th>

      <th>
        iOS

        Experimental
      </th>

      <th>
        Android

        Experimental
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        Real OS windows
      </td>

      <td>
        First-class
      </td>

      <td>
        First-class
      </td>

      <td>
        First-class
      </td>

      <td>
        Works with caveats — full-screen app in the toolkit-owned UIKit host; single window (footnote 1)
      </td>

      <td>
        Works with caveats — full-screen app in the toolkit-owned Android host; single window (footnote 1)
      </td>
    </tr>

    <tr>
      <td>
        Native rendering
      </td>

      <td>
        First-class — Metal presentation with OS scroll physics (footnote 2)
      </td>

      <td>
        Works with caveats — deterministic software renderer, GDI blit; per-monitor DPI aware (footnote 2)
      </td>

      <td>
        Works with caveats — deterministic software renderer, cairo blit (footnote 2)
      </td>

      <td>
        Works with caveats — deterministic software renderer; the toolkit host presents through Metal (footnote 2)
      </td>

      <td>
        Works with caveats — deterministic software renderer; the toolkit host copies pixels into the surface (footnote 2)
      </td>
    </tr>

    <tr>
      <td>
        Text
      </td>

      <td>
        First-class — the SDK's own TrueType pipeline plans every layout; CoreText measurement and Metal packet text resolve the same bundled faces (footnote 3)
      </td>

      <td>
        First-class — bundled faces ink through the deterministic software renderer (footnote 3)
      </td>

      <td>
        First-class — bundled faces ink through the deterministic software renderer (footnote 3)
      </td>

      <td>
        First-class — the toolkit host presents the same reference-renderer pixels the desktop software hosts ink (footnote 3)
      </td>

      <td>
        First-class — the toolkit host presents the same reference-renderer pixels the desktop software hosts ink (footnote 3)
      </td>
    </tr>

    <tr>
      <td>
        Registered fonts
      </td>

      <td>
        First-class — app-registered TrueType faces (CJK included); registered bytes reach CoreText at registration, so host measurement and drawn text resolve the exact face (footnote 3)
      </td>

      <td>
        First-class — registered faces (CJK included) ink through the deterministic software renderer (footnote 3)
      </td>

      <td>
        First-class — registered faces (CJK included) ink through the deterministic software renderer (footnote 3)
      </td>

      <td>
        Not available — registered fonts unverified: the ink path is the same reference renderer, but no mobile test registers a font and host text measurement has no registered-font seam (footnote 3)
      </td>

      <td>
        Not available — registered fonts unverified: the ink path is the same reference renderer, but no mobile test registers a font and host text measurement has no registered-font seam (footnote 3)
      </td>
    </tr>

    <tr>
      <td>
        Pointer, keyboard & IME input
      </td>

      <td>
        First-class
      </td>

      <td>
        Works with caveats — IME composition is mapped; real-hardware IME verification is pending (footnote 4)
      </td>

      <td>
        First-class — pointer, keyboard, scroll, IME composition, HiDPI
      </td>

      <td>
        Works with caveats — touch, system keyboard, and IME through the toolkit host (footnote 4)
      </td>

      <td>
        Works with caveats — touch, soft keyboard, and IME through the toolkit host (footnote 4)
      </td>
    </tr>

    <tr>
      <td>
        Menus
      </td>

      <td>
        First-class — native app menus and native context menus (NSMenu) (footnote 5)
      </td>

      <td>
        First-class — native app menus and native context menus (TrackPopupMenu) (footnote 5)
      </td>

      <td>
        First-class — native app menus and native context menus (GtkPopoverMenu) (footnote 5)
      </td>

      <td>
        Not available
      </td>

      <td>
        Not available
      </td>
    </tr>

    <tr>
      <td>
        System tray
      </td>

      <td>
        First-class
      </td>

      <td>
        First-class
      </td>

      <td>
        Not available — returns UnsupportedService until a portable status-notifier implementation is selected (footnote 6)
      </td>

      <td>
        Not available
      </td>

      <td>
        Not available
      </td>
    </tr>

    <tr>
      <td>
        System notifications
      </td>

      <td>
        First-class — same-id replacement and a native action button; actions dispatch while the app is running (footnote 11)
      </td>

      <td>
        Works with caveats — one in-flight legacy balloon, so every new notification replaces the current one; there is no custom button, and clicking the notification invokes its labeled action (footnote 11)
      </td>

      <td>
        First-class — same-id replacement and a native action button; actions dispatch while the app is running (footnote 11)
      </td>

      <td>
        Not available
      </td>

      <td>
        Not available
      </td>
    </tr>

    <tr>
      <td>
        Menu-bar app lifecycle (

        `close_policy = "hide"`

        )
      </td>

      <td>
        First-class — close hides, the Dock reopen re-shows, tray Open/Quit drive showWindow/quitApp
      </td>

      <td>
        First-class — close hides (SW_HIDE); the tray icon is the re-show affordance
      </td>

      <td>
        Not available — no status item exists to bring a hidden window back, so the declaration is refused at build/create time with a teaching (footnote 5)
      </td>

      <td>
        Not available
      </td>

      <td>
        Not available
      </td>
    </tr>

    <tr>
      <td>
        Web content (WebViews)
      </td>

      <td>
        First-class — system WebView by default; optional bundled Chromium via CEF (footnote 7)
      </td>

      <td>
        Works with caveats — system WebView (WebView2); needs the WebView2 runtime, preinstalled on current Windows (footnote 7)
      </td>

      <td>
        First-class — system WebView (footnote 7)
      </td>

      <td>
        Embed-level — system WebView workspace inside your host app (footnote 7)
      </td>

      <td>
        Embed-level — system WebView workspace inside your host app (footnote 7)
      </td>
    </tr>

    <tr>
      <td>
        Packaging
      </td>

      <td>
        First-class — .app bundle, DMG, generated icons (footnote 8)
      </td>

      <td>
        Works with caveats — directory artifact with icons and file-type registration; installer is future work (footnote 8)
      </td>

      <td>
        Works with caveats — install tree with desktop entry, icons, MIME metadata; AppImage and packages are future work (footnote 8)
      </td>

      <td>
        Works with caveats — complete generated Xcode project, archive-ready with zero edits; signing manual, no .ipa emit (footnote 8)
      </td>

      <td>
        Works with caveats — complete generated host project plus a debug APK assembled with zero edits; store signing manual (footnote 8)
      </td>
    </tr>

    <tr>
      <td>
        Code signing
      </td>

      <td>
        First-class — ad-hoc and identity signing with entitlements; notarization is a documented manual step (footnote 9)
      </td>

      <td>
        Not available
      </td>

      <td>
        Not available
      </td>

      <td>
        Not available
      </td>

      <td>
        Not available
      </td>
    </tr>

    <tr>
      <td>
        Automation
      </td>

      <td>
        First-class
      </td>

      <td>
        First-class — driven live on a real Windows desktop and in CI under emulation (footnote 10)
      </td>

      <td>
        First-class — driven live under Xvfb: waits, assertions, input, screenshots (footnote 10)
      </td>

      <td>
        Works with caveats — file-based snapshots and actions inside the app container, exercised on the simulator (footnote 10)
      </td>

      <td>
        Works with caveats — file-based snapshots and actions inside the app files directory, exercised on the emulator (footnote 10)
      </td>
    </tr>
  </tbody>
</table>

1. iOS and Android canvas apps run full screen inside toolkit-owned hosts, which the SDK builds ON the [embed C ABI](/docs/embed) — `native dev` and `native package` with `--target ios|android` generate everything from the app manifest, so the app project carries zero host code. Multi-window scenes are desktop-only. Embedding the runtime in a host app you control, driven over the same embed C ABI, works on both platforms and shares the mobile experimental status.
2. macOS presents through Metal and hands scrolling to OS scroll drivers. Windows presents representable retained packets through Direct2D/DirectWrite and falls back to the deterministic software renderer for unsupported commands, transparent layered windows, or unavailable GPU resources. Linux uses the software renderer through its platform blit path. Frame events report the concrete `metal`, `direct2d`, or `software` backend. The iOS toolkit host reads the software renderer's pixels over the ABI and presents them through Metal with the macOS host's presentation discipline — presents are gated on the canvas revision (an idle app acquires no drawables and uploads nothing), the drawable is acquired only after the frame's CPU work is done, and the pump pauses in the background and re-presents the retained canvas on return; the Android toolkit host copies the same pixels into its surface's window buffer (mobile GPU rendering is a later phase); embed hosts present the pixels in their own surfaces.
3. The SDK's own TrueType pipeline — parsing, outlines, rasterization — is text rendering's shared reference path on every platform (goldens, screenshots, and software presents all ink through it), and apps can [register additional faces](/docs/fonts) (a CJK face is the canonical case) that both renderers resolve exactly like the bundled ones. macOS hands registered bytes to CoreText, and Windows hands them to DirectWrite, so packet text presentation resolves the same face the reference renderer inks. Linux and Windows software fallback ink registered outlines directly; the full font-registry suite runs in CI on Linux, and CI additionally runs it natively on a Windows runner, including a receipt test that registers a committed subsetted CJK face and proves Chinese text renders as real glyphs, not tofu. Mobile hosts present the same reference-rendered pixels, but no mobile test registers a font today and the mobile hosts' text measurement has no registered-font seam, so registered fonts there are stated as unverified rather than supported.
4. Windows maps native IME composition onto the shared IME events, with real-hardware IME verification still pending. The iOS toolkit host forwards touch, the system keyboard, and IME composition, verified with real injected input on the simulator; the Android toolkit host forwards touch, shows and hides the soft keyboard from the runtime's focus state, and routes committed text and IME composition through the same embed IME events, verified with injected input on the emulator.
5. App menus and context menus are native on all three desktops: macOS presents `NSMenu`, Windows `TrackPopupMenu` (the tray menu's popup path), Linux `GtkPopoverMenu`. Hosts without a native context-menu presenter — the mobile toolkit hosts and embed hosts today — present the same declared context menu as an anchored canvas surface at the click point; authors declare one menu either way.
6. Tray support is implemented on macOS (`NSStatusItem`) and Windows; Linux tray calls return `UnsupportedService` until a portable status-notifier implementation is selected.
7. Web engines only apply to apps that embed web content; native-rendered apps carry none. The system WebView is the default engine on every desktop platform; bundled Chromium through CEF is available on macOS only, and Linux/Windows Chromium builds fail early instead of silently substituting an engine. The Windows system engine is WebView2: its Evergreen runtime ships with Windows 11 and current Windows 10 (older machines need the runtime installer; `native doctor` checks for it), and the build stages the vendored loader next to the executable automatically. See [Web Engines](/docs/web-engines). The mobile shell examples embed the platform WebView as the content workspace.
8. `native package` targets all five platforms: macOS gets a `.app` bundle plus a styled drag-to-Applications DMG with `--archive`, Linux an install tree, Windows a distributable directory with a per-user file-type registration script, iOS a complete generated Xcode project — toolkit host sources, Info.plist, asset catalog, shared scheme, and the device-slice embed library — that `xcodebuild archive` builds from the generated project (code signing stays a manual step, like notarization), and Android a complete generated host project whose debug APK assembles from the generated project, directly with the SDK's build tools (store signing keys stay a manual step). Every platform's icons generate from one square source image. See [Packaging](/docs/packaging).
9. macOS signing supports `adhoc` and `identity` modes with entitlements; `native package --notarize` can submit and staple the signed artifact. No signing tooling exists yet for the other platforms. See [Code Signing](/docs/packaging/signing).
10. The automation server is a file-based protocol the runtime serves on every desktop platform: snapshots, assertions, synthetic input, screenshots, record/replay. Engine screenshots render through the deterministic CPU reference renderer on every platform, Use the same runner image for golden comparisons because text measurement can differ between hosts. Mobile exposes accessibility snapshots and actions through the embed ABI; the iOS and Android toolkit hosts serve the same file-based protocol inside the app's data container when launched with automation enabled.
11. Desktop notification identifiers are app-local replacement keys. macOS and Linux expose the requested action as a native button. The current Windows host uses the in-box `Shell_NotifyIcon` balloon surface, which has no app-defined action button; it includes the label in the notification and treats a notification click as activation. All three deliver action commands only to the process that issued the notification. Linux notification shells may D-Bus-activate an exited app when a persisted notification is clicked, but its process-scoped action token is stale and cannot dispatch a command into the new runtime; macOS and Windows cold-start command activation is not wired.

## Desktop Hosts

<table>
  <thead>
    <tr>
      <th>
        Area
      </th>

      <th>
        macOS system WebView
      </th>

      <th>
        macOS Chromium
      </th>

      <th>
        Linux system WebView
      </th>

      <th>
        Linux Chromium
      </th>

      <th>
        Windows system WebView
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        Main WebView
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported
      </td>
    </tr>

    <tr>
      <td>
        Child WebViews
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported
      </td>
    </tr>

    <tr>
      <td>
        Native views
      </td>

      <td>
        Supported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported
      </td>
    </tr>

    <tr>
      <td>
        Native control commands
      </td>

      <td>
        Supported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported
      </td>
    </tr>

    <tr>
      <td>
        App menus
      </td>

      <td>
        Supported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported
      </td>
    </tr>

    <tr>
      <td>
        Native context menus
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Anchored fallback
      </td>

      <td>
        Supported
      </td>
    </tr>

    <tr>
      <td>
        System tray
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported
      </td>
    </tr>

    <tr>
      <td>
        Keyboard shortcuts
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported
      </td>
    </tr>

    <tr>
      <td>
        Dialogs
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported
      </td>
    </tr>

    <tr>
      <td>
        Clipboard text
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported
      </td>
    </tr>

    <tr>
      <td>
        Clipboard rich data
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported
      </td>
    </tr>

    <tr>
      <td>
        Open URL / reveal path
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported
      </td>
    </tr>

    <tr>
      <td>
        Notifications
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported
      </td>
    </tr>

    <tr>
      <td>
        Recent documents
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported
      </td>
    </tr>

    <tr>
      <td>
        Credentials
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported when a secret service is present
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported
      </td>
    </tr>

    <tr>
      <td>
        File drops
      </td>

      <td>
        Supported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported
      </td>
    </tr>

    <tr>
      <td>
        App activation events
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Supported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported
      </td>
    </tr>

    <tr>
      <td>
        Audio playback + streaming
      </td>

      <td>
        Supported (one AVPlayer for local files, cache entries, and streams)
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported when GStreamer with playbin is present (runtime-loaded, probed live)
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported (one Media Foundation session)
      </td>
    </tr>

    <tr>
      <td>
        Audio spectrum analysis
      </td>

      <td>
        Supported (MTAudioProcessingTap + vDSP on the single player)
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported when GStreamer's

        `spectrum`

        element is present (gst-plugins-good, probed live)
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported on Windows 10 2004+ (process-scoped WASAPI loopback of this app only, probed live)
      </td>
    </tr>

    <tr>
      <td>
        Microphone + system audio capture
      </td>

      <td>
        Supported (AVAudioEngine microphone; ScreenCaptureKit system mix on macOS 13+; user consent required)
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Unsupported
      </td>

      <td>
        Supported (default capture endpoint + WASAPI render loopback)
      </td>
    </tr>
  </tbody>
</table>

"Anchored fallback" for native context menus means the declared menu still presents — as an anchored canvas surface at the click point — because that host has no native menu presenter. All three desktop system engines present natively (`NSMenu` on macOS, `TrackPopupMenu` on Windows, `GtkPopoverMenu` on Linux). Authors declare one menu either way; the platform decides presentation.

Audio spectrum analysis reads the app's OWN playback and nothing else: macOS taps the app's single player pre-effects, Windows captures the app's own audio session through process-scoped loopback (never system-wide — other apps' audio can never appear in the bands), and Linux analyzes inside the app's own playbin. Hosts that cannot analyze report `audio_spectrum` unsupported and simply never deliver `.spectrum` events; see the capabilities page for the band shape and cadence contract.

## How Support Is Verified

macOS is the primary development platform and carries the deepest support. The Linux and Windows columns are not aspirational: the repository carries reproducible live-verification loops that build every showcase app and drive it for real. `tools/linux-truth` runs the apps in real windows against the platform toolkit under Xvfb — clicks, keys, wheel input, multi-window flows, resize clamps, window-close paths, and both engine and X-server screenshots. `tools/windows-truth` drives the same scenarios on a real Windows desktop, plus clipboard round-trips, effect-stream cancellation, record/replay, and launching the packaged artifact. CI additionally runs headless Linux and Windows canvas smokes on every change. iOS is exercised on the simulator through the toolkit host and the embed library — the `native dev --target ios` loop launches real apps, and the input and layout verification scripts inject hardware-true touches and keyboard events; a fresh `native package --target ios` output is archived with `xcodebuild` as part of verification. Android is exercised on the emulator the same way: the `native dev --target android` loop assembles, installs, and launches real apps, input is injected over adb (taps, keys, soft-keyboard text), and a fresh `native package --target android` output assembles the debug APK; the embed ABI additionally cross-compiles for both Android architectures in CI.

## Runtime Queries

Use `Runtime.supports(...)` when native code needs to branch on the current host:

```zig
if (runtime.supports(.native_views)) {
    try runtime.createView(.{
        .label = "sidebar",
        .kind = .sidebar,
    });
}
```

JavaScript can query the same support model through the built-in bridge when `js_window_api` or an explicit `builtin_bridge` policy allows `native-sdk.platform.supports`:

```javascript
const hasTray = await window.zero.platform.supports("tray");
```

Feature names match the Zig `PlatformFeature` enum and the TypeScript `NativeSdkPlatformFeature` union. JavaScript callers may use either snake\_case names such as `native_views`, `microphone_capture`, and `system_audio_capture` or camelCase aliases such as `nativeViews`, `microphoneCapture`, and `systemAudioCapture`. Unsupported operations still reject explicitly if called; support checks are intended for choosing UI affordances before making those calls.

## Native Surfaces

The macOS, Linux, and Windows system-WebView hosts support `toolbar`, `titlebar_accessory`, `sidebar`, `statusbar`, `split`, `stack`, `button`, `icon_button`, `list_item`, `checkbox`, `toggle`, `segmented_control`, `text_field`, `search_field`, `label`, `spacer`, and `progress_indicator` native view kinds. Their `gpu_surface` children present through Metal on macOS, retained Direct2D/DirectWrite packets on Windows, and the CPU reference renderer plus a Cairo pixel blit on Linux. Windows retains the GDI pixel path as an exact fallback.

`ViewKind.webview` is the compatibility path for WebView-backed views. Existing `runtime.createWebView(...)` and `window.zero.webviews.*` APIs remain available.

Window chrome control spans all three desktop hosts: `titlebar = "hidden_inset"`/`"hidden_inset_tall"`, the `window-drag` region channel, and the `on_chrome` inset hook are implemented on the macOS hosts (AppKit and Chromium), the Win32 host (the full system frame stays: the caption band is reclaimed for the app while DWM composites the real min/max/close buttons, and drag regions answer the native hit test as caption), and the GTK host (client-side decorations: a header bar with the desktop-themed window controls, honoring the user's `gtk-decoration-layout` for button side and order, with drag regions consumed at the press gesture). On every host the `on_chrome` hook reports where window controls land — top band height plus a leading or trailing controls inset — so a header pads the correct edge with no per-platform code. Mobile embed hosts answer the same `on_chrome` channel with the viewport's safe-area insets (notch, status bar, home indicator), and subscribing transfers safe-area padding ownership to the app — see [Mobile](#mobile).

Chromium builds are currently enabled on macOS only. Linux and Windows Chromium host files are placeholders and build/package tooling rejects those targets until the native hosts are implemented.

## Mobile

Experimental Mobile support is experimental — the host tier and the embedding path alike. Everything below is real and verified — iOS on the simulator, Android on the emulator — but the APIs and tooling on these platforms may still change, and desktop is the mature surface. The label describes maturity: what each platform does today is stated exactly.

### The iOS host tier

iOS canvas apps ship without you ever opening Xcode as an editor: the toolkit owns the entire iOS application, built on the same embed C ABI that standalone embedding uses. `native dev --target ios` builds the app's embed static library for the simulator, wraps it in the SDK's UIKit host — a full-screen canvas surface view with touch/keyboard/IME forwarding, CoreText text measurement, and safe-area reporting — installs and launches it via `simctl`, and streams the app log to your terminal. `native package --target ios` emits a complete, deterministic Xcode project around the same host (generated Info.plist, asset catalog from the single-source icon pipeline, shared scheme, device-slice library) that `xcodebuild archive` builds from the generated project; code signing stays a manual step, separately from packaging.

Safe-area insets (notch, status bar, home indicator) arrive through the same window-chrome channel macOS uses for the titlebar band: an app that subscribes to `on_chrome` pads with one code path on every platform and owns the safe-area padding; an app without the hook keeps the automatic runtime inset. The full audio effect family plays for real: the host registers the platform audio service over the embed ABI — AVAudioPlayer for local files and verified cache entries, AVPlayer for progressive URL streams filling the track cache in the app container's `Library/Caches/audio/` — with a playback audio session (activated on first play; a system interruption pauses the transport and reports the paused state through the ordinary event stream). Background audio and now-playing-center integration are not wired. Runtime-registered images decode for real too: the host registers the platform image decoder over the embed ABI — CGImageSource (ImageIO), the same codec family the macOS host decodes through — so `fx.registerImageBytes` registers real pixels (the soundboard's committed album covers, fetched avatars) instead of declining to the initials fallback. Current limitations: frames render through the deterministic CPU reference renderer (GPU rendering on iOS is a later phase), markup hot reload does not reach the simulator yet (edit, then rerun `native dev --target ios`), and device workflows beyond `xcodebuild archive` are not toolkit-managed.

### The Android host tier

Android canvas apps get the same ownership: the toolkit owns the entire Android application, built on the same embed C ABI. `native dev --target android` builds the app's embed static library for `aarch64-linux-android`, compiles the SDK's Android host around it — an edge-to-edge canvas `SurfaceView` with touch/soft-keyboard/IME forwarding, Paint-backed text measurement, and safe-area/keyboard inset reporting — assembles a debug-signed APK, installs and launches it on a running (or freshly booted) emulator via `adb`, and streams the app log to your terminal. `native package --target android` emits a complete generated host project and assembles the same debug APK with zero edits, directly with the Android SDK's build tools (aapt2, javac, d8, zipalign, apksigner) and the NDK compiler — no build-system project to maintain, no plugin/version matrix, and the dev and package paths share one assembly so they cannot drift.

Display-cutout and system-bar insets ride the same `on_chrome` window-chrome channel, and the soft keyboard follows the runtime's focus state: the host shows the keyboard when an editable widget takes focus, routes committed text and composition through the embed IME events, and reports the keyboard's viewport inset so layout clears it (apps that own safe areas keep only the keyboard's residual overlap). Rotation keeps the activity alive, so the embedded runtime survives orientation changes as a resize. The full audio effect family plays for real: the host registers the platform audio service over the embed ABI — one `MediaPlayer` for local files, verified cache entries, and progressive URL streams filling the track cache in the app data directory's `cache/audio/` — with audio focus (requested on play; a focus loss pauses the transport and reports the paused state through the ordinary event stream, and regain never auto-resumes). Media-session and notification integration (lock screen controls, background playback beyond the cached process) are not wired. Runtime-registered images decode for real too: the host registers the platform image decoder over the embed ABI — BitmapFactory, the platform's in-box codec stack — so `fx.registerImageBytes` registers real pixels (the soundboard's committed album covers, fetched avatars) instead of declining to the initials fallback. Current limitations: one ABI ships today (`arm64-v8a`, every Android 11+ device; minSdk 30), the APK is debug-signed (store distribution keys and app bundles stay a manual step, separately from packaging), and markup hot reload does not reach the device yet (edit, then rerun `native dev --target android`).

### Embedded hosts

Embedding works on both mobile platforms and carries the same experimental status as the rest of the mobile experience. The mobile examples own native header/navigation layout in UIKit or Android views, embed a `WKWebView` or Android `WebView` as the workspace, and drive the Native SDK through the C/JNI lifecycle, activation, resize, and command calls documented in [Embedded App](/docs/embed).

<table>
  <thead>
    <tr>
      <th>
        Area
      </th>

      <th>
        iOS embedded host
      </th>

      <th>
        Android embedded host
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        Native header/navigation
      </td>

      <td>
        UIKit header with Back and Refresh command buttons
      </td>

      <td>
        Android view header with Back and Refresh command buttons
      </td>
    </tr>

    <tr>
      <td>
        WebView content area
      </td>

      <td>
        `WKWebView`

        workspace
      </td>

      <td>
        Android

        `WebView`

        workspace
      </td>
    </tr>

    <tr>
      <td>
        Runtime lifecycle
      </td>

      <td>
        `native_sdk_app_create`

        ,

        `start`

        ,

        `activate`

        ,

        `deactivate`

        ,

        `stop`

        , and

        `destroy`

        from Swift
      </td>

      <td>
        JNI bridge for

        `create`

        ,

        `start`

        ,

        `activate`

        ,

        `deactivate`

        ,

        `stop`

        , and

        `destroy`
      </td>
    </tr>

    <tr>
      <td>
        Resize and orientation
      </td>

      <td>
        `viewDidLayoutSubviews`

        forwards WebView size and screen scale
      </td>

      <td>
        Orientation/screen-size changes stay in the activity;

        `surfaceChanged`

        forwards size, density, and

        `Surface`
      </td>
    </tr>

    <tr>
      <td>
        Keyboard avoidance
      </td>

      <td>
        Keyboard frame notifications adjust the WebView bottom constraint
      </td>

      <td>
        `windowSoftInputMode="adjustResize"`

        lets Android relayout the content area
      </td>
    </tr>

    <tr>
      <td>
        Back/navigation command
      </td>

      <td>
        Native Back button dispatches

        `mobile.back`
      </td>

      <td>
        Native Back button and system Back dispatch

        `mobile.back`
      </td>
    </tr>

    <tr>
      <td>
        Touch forwarding
      </td>

      <td>
        UIKit/WebKit own touch handling for the WebView workspace
      </td>

      <td>
        `MotionEvent`

        pointer data is forwarded through

        `native_sdk_app_touch`
      </td>
    </tr>

    <tr>
      <td>
        Generic

        `ShellView`

        mapping
      </td>

      <td>
        Mobile shell metadata feeds generated UIKit host config for header labels, command buttons, and WebView workspace
      </td>

      <td>
        Mobile shell metadata feeds generated Android host config for header labels, command buttons, and WebView workspace
      </td>
    </tr>
  </tbody>
</table>

Mobile hosts own safe areas, orientation, keyboard avoidance, back gestures, and platform lifecycle integration. The current mobile mapping is package-time shell metadata for native header/workspace structure; dynamic generic native view bridge commands should still be treated as desktop APIs until UIKit and Android runtime view mutation is added.

Canvas-scene apps also run through a hand-written embed host: `addMobileLib` compiles a `Model`/`Msg`/`update`/`view` app into the embed static library with a single `gpu_surface` scene, frames render through the deterministic CPU reference renderer, and the host shim blits the presented pixels (`native_sdk_app_render_pixels`) into its own surface. The `examples/mobile-canvas` iOS shim — the same architecture the toolkit's own UIKit host uses — is exercised on the simulator: rendering, safe-area layout, real touch/keyboard/IME input, and accessibility snapshots. The Android `NativeActivity` shim cross-compiles for both Android arches from the same library build.

`gpu_surface` is implemented for the macOS system-WebView host as a Metal-backed child surface, for the Windows system-WebView host as a retained Direct2D/DirectWrite child surface, and for the Linux system-WebView host through the deterministic CPU reference renderer plus a GTK pixel blit. Windows frame events report `backend=direct2d` while binary canvas packets remain representable; unrepresentable commands, transparent layered windows, or an unavailable Direct2D device fall back to the same CPU reference renderer plus a GDI DIB blit and report `backend=software`. Linux reports `backend=software`, and a manifest that declares another backend (for example `gpu_backend = "metal"`) falls back to software there instead of erroring. Windows maps native IME composition (`WM_IME_COMPOSITION`) onto the same shared IME events the macOS and Linux hosts emit — inline preedit, cursor position, and the commit contract — with real-hardware IME verification still pending. Other current hosts report unsupported operations for that view kind and `runtime.supports(.gpu_surfaces)` / `window.zero.platform.supports("gpuSurfaces")` return `false`.

## Related Docs

- [Native Controls](/docs/native-controls)
- [Web Engines](/docs/web-engines)
- [Capabilities](/docs/capabilities)
- [Builtin Commands](/docs/bridge/builtin-commands)

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Quick Start
url: "https://native-sdk.dev/docs/quick-start"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Install the CLI, create a TypeScript app, and run it in a native window. This guide also covers markup reloads, core checks, and release builds. To write the app core in Zig, use the `zig-core` template.

## Prerequisites

- macOS 11 or newer, Linux, or Windows
- Node.js 24+ for the default TypeScript scaffold — the TypeScript frontend (the checker) and core dev loop run under it at build and dev time; the SDK installs a checksum-verified scriptc 0.3.2 release alongside its matching compiler API. The binary you ship carries no JS runtime. A Zig-core app (`--template zig-core`) needs Node only when it declares relational SQLite, whose schema checker and migration generator run at build time.

## Get the CLI

```bash
npm install -g @native-sdk/cli
native version
```

The CLI configures the SDK and toolchain:

- **The SDK location.** Apps build against the SDK the CLI ships with — `native init` records the path automatically (override with `--framework <sdk path>`).
- **The Zig toolchain.** `native dev|build|test` use the Zig on your PATH when its version is compatible, and otherwise offer to download the pinned version into `~/.native/toolchains/` (checksum-verified; pass `--yes` to skip the prompt in scripts). The toolkit requires Zig 0.16.0 — if you learned Zig on an older version, [Zig 0.16 Notes](/docs/zig) maps the standard-library changes.

## Create an app

```bash
native init my_app
cd my_app
```

The CLI generates a counter app and manages its build graph under `.native/build/`. The project contains:

<table>
  <thead>
    <tr>
      <th>
        File
      </th>

      <th>
        Purpose
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `src/core.ts`
      </td>

      <td>
        The logic:

        `Model`

        ,

        `Msg`

        ,

        `update`

        — plain TypeScript, compiled to native code at build time
      </td>
    </tr>

    <tr>
      <td>
        `src/app.native`
      </td>

      <td>
        The entire UI: elements, layout, bindings, and message dispatch
      </td>
    </tr>

    <tr>
      <td>
        `app.json`
      </td>

      <td>
        App manifest: identity, window and view declarations, permissions, security policy. Its

        `$schema`

        enables editor completion and validation; existing

        `app.zon`

        manifests remain supported.
      </td>
    </tr>

    <tr>
      <td>
        `assets/icon.png`
      </td>

      <td>
        The app icon source: one square image packaging turns into every platform's icon artifacts
      </td>
    </tr>

    <tr>
      <td>
        `package.json`

        ,

        `tsconfig.json`
      </td>

      <td>
        The editor surface: stock editor TypeScript resolves

        `@native-sdk/core`

        with full IntelliSense, and the tsconfig mirrors the checker's own compiler options
      </td>
    </tr>

    <tr>
      <td>
        `.gitignore`

        ,

        `README.md`
      </td>

      <td>
        Ignores for generated directories, and the commands on this page
      </td>
    </tr>
  </tbody>
</table>

The CLI copies `@native-sdk/core` into `node_modules` for editor completion and refreshes it during check, development, and build commands. Builds use the SDK selected by the CLI rather than this editor copy.

The build detects the core language from the source files. Use `native init my_app --template zig-core` for `src/main.zig` and generated Zig tests in `src/tests.zig`. Add `--full` to either template to generate app-owned build files.

## Run it

```bash
native dev
```

The first run compiles the app and the SDK. A native window opens with a counter. Its view is in `src/app.native`:

```html title="src/app.native"
<column gap="12" padding="16">
  <row gap="8" cross="center">
    <text grow="1">Counter</text>
    <button size="sm" variant="ghost" on-press="reset">Reset</button>
  </row>
  <row gap="8" main="center" cross="center" grow="1">
    <button variant="secondary" on-press="decrement">-</button>
    <text>{count}</text>
    <button variant="primary" on-press="increment">+</button>
  </row>
  <row gap="8" cross="center">
    <switch checked="{ticking}" on-toggle="toggle_ticking">Tick every second</switch>
    <text grow="1">ticks {tickCount}</text>
    <button size="sm" on-press="stamp">Stamp</button>
  </row>
  <status-bar>total: {total} | stamped: {stampedMs}ms</status-bar>
</column>
```

Markup reads values (`{count}`) and dispatches messages (`on-press="increment"`). The core changes state through `update` and requests external work through effects. The generated counter also uses a clock effect and a one-second timer. The TypeScript template declares these through `Cmd` and `Sub`; the Zig template uses `fx.wallMs` and `fx.startTimer`:

```ts title="src/core.ts"
import { Cmd, Sub } from "@native-sdk/core";

export interface Model {
  readonly count: number;
  readonly ticking: boolean;
  readonly tickCount: number;
  readonly stampedMs: number;
}

export type Msg =
  | { readonly kind: "increment" }
  | { readonly kind: "decrement" }
  | { readonly kind: "reset" }
  | { readonly kind: "toggle_ticking" }
  | { readonly kind: "stamp" }
  | { readonly kind: "stamped"; readonly at: number }
  | { readonly kind: "tick"; readonly at: number };

// `tick` and `stamped` are dispatched by the host (timer fires and the
// Cmd.now result), never from markup - this list keeps `native check`'s
// unbound-state lint honest about that.
export const viewUnbound = ["tick", "stamped"] as const;

export function initialModel(): Model {
  return { count: 0, ticking: false, tickCount: 0, stampedMs: -1 };
}

// Exported single-model helpers become bindings too: `{total}` in
// app.native reads this.
export function total(model: Model): number {
  return model.count + model.tickCount;
}

export function update(model: Model, msg: Msg): Model | [Model, Cmd<Msg>] {
  switch (msg.kind) {
    case "increment":
      return { ...model, count: model.count + 1 };
    case "decrement":
      return { ...model, count: model.count - 1 };
    case "reset":
      return { ...model, count: 0, tickCount: 0 };
    case "toggle_ticking":
      return { ...model, ticking: !model.ticking };
    case "stamp":
      // Effects are data: the host performs this after commit and
      // dispatches `stamped` with the time.
      return [model, Cmd.now("stamped")];
    case "stamped":
      return { ...model, stampedMs: msg.at };
    case "tick":
      return { ...model, tickCount: model.tickCount + 1 };
  }
}

// Recurring effects are declared from the model: while `ticking` holds,
// the host fires `tick` every second; flip it off and the timer stops.
export function subscriptions(model: Model): Sub<Msg> {
  if (!model.ticking) return Sub.none;
  return Sub.timer("tick", 1000, "tick");
}
```

```zig title="src/main.zig"
pub const Msg = union(enum) {
    increment,
    decrement,
    reset,
    toggle_ticking,
    stamp,
    tick: native_sdk.EffectTimer,

    // `tick` is dispatched by the host (the repeating timer fires),
    // never from markup - this keeps the unbound-state lint honest
    // about that.
    pub const view_unbound = .{"tick"};
};

pub const Model = struct {
    count: i64 = 0,
    ticking: bool = false,
    tick_count: i64 = 0,
    stamped_ms: i64 = -1,

    // Public single-model helpers become bindings too: `{total}` in
    // app.native reads this.
    pub fn total(model: *const Model) i64 {
        return model.count + model.tick_count;
    }
};

pub const Effects = native_sdk.Effects(Msg);

/// The repeating tick's effects-channel key: starting an active key
/// replaces the timer in place, so toggling never double-registers.
pub const tick_timer_key: u64 = 1;

pub fn update(model: *Model, msg: Msg, fx: *Effects) void {
    switch (msg) {
        .increment => model.count += 1,
        .decrement => model.count -= 1,
        .reset => {
            model.count = 0;
            model.tick_count = 0;
        },
        .toggle_ticking => {
            model.ticking = !model.ticking;
            // Recurring effects are timers on the effects channel: while
            // `ticking` holds, the host fires `tick` every second; flip
            // it off and the timer stops.
            if (model.ticking) {
                fx.startTimer(.{
                    .key = tick_timer_key,
                    .interval_ms = 1000,
                    .mode = .repeating,
                    .on_fire = Effects.timerMsg(.tick),
                });
            } else {
                fx.cancelTimer(tick_timer_key);
            }
        },
        // The journaled clock read - deterministic under session replay,
        // the Zig equivalent of the TypeScript starter's `Cmd.now`.
        .stamp => model.stamped_ms = fx.wallMs(),
        .tick => |timer| {
            if (timer.outcome != .fired) return;
            model.tick_count += 1;
        },
    }
}
```

Bindings use field names exactly as declared: `{tickCount}` in TypeScript and `{tick_count}` in Zig. Helpers such as `{total}` provide derived values. See [App Model](/docs/app-model) for the runtime loop and [TypeScript Cores](/docs/typescript) for core authoring.

## Edit while it runs

`src/app.native` is embedded into the binary and watched while `native dev` runs — `native dev` runs a Debug build by default, which is what arms the hot-reload watcher. Edit it — change a label, add a button — and the window updates while preserving the count. Parse failures keep the last good view on screen. A `src/core.ts` edit is different: the core rebuilds through the external core compiler and the app restarts — use `native dev --core` (next section).

<span id="the-fastest-loop-the-core-under-node" />

## Run the core under Node.js

`native dev --core` runs `src/core.ts` under Node.js with a virtual host. Send messages as JSON lines, inspect the committed model and effects, and advance a virtual clock to fire timers. This mode does not open a window or render the UI:

```bash
printf '%s\n' '{"kind":"increment"}' '{"kind":"toggle_ticking"}' '{"advance":3000}' | native dev --core
```

```
native dev --core: the core-logic loop under node (update/effects, virtual clock) - not a renderer; `native dev` runs the app
model {"count":0,"ticking":false,"tickCount":0,"stampedMs":-1}
model {"count":1,"ticking":false,"tickCount":0,"stampedMs":-1}
model {"count":1,"ticking":true,"tickCount":0,"stampedMs":-1}
sub arm tick every 1000ms -> tick
fire tick -> tick @ 1000
model {"count":1,"ticking":true,"tickCount":1,"stampedMs":-1}
fire tick -> tick @ 2000
model {"count":1,"ticking":true,"tickCount":2,"stampedMs":-1}
fire tick -> tick @ 3000
model {"count":1,"ticking":true,"tickCount":3,"stampedMs":-1}
```

The core file can run under Node.js during development and compile to native code for the app. Pair it with `--script msgs.ndjson --watch` to replay a scenario on every edit.

## Check it

```bash
native check
```

`native check` validates the whole tree without building anything: `src/core.ts` runs the subset checker (typecheck plus the app-core rules, with diagnostics that explain the rule and a suggested fix), then every `.native` file under `src/` and `app.json`:

```
info[manifest.valid]: app.json is valid
src/app.native: ok
web layer: none (inferred: nothing in app.zon declares web use)
checked 1 markup file, app.json and src/core.ts (subset checker clean, views typed against the core)
```

The markup pass compiles your views against the core with the same view compiler the build uses, so bindings, iterables, and message tags are verified against the core's `Model`/`Msg`, and model state no view uses is reported as a warning. Markup errors come back with `file:line:column` and a teaching message (`native markup lsp` provides the same diagnostics plus completion and hover in your editor). `native test` runs the app's test suite; the Zig template additionally scaffolds `src/tests.zig` — full-loop UI tests that click buttons through typed dispatch, headless, on any machine. See [Testing](/docs/testing) for the full tiers, including driving the live app from the outside with [automation](/docs/automation).

## Build a release binary

```bash
native build
```

This produces an optimized binary and tells you where it landed:

```
built zig-out/bin/my-app (ReleaseFast)
```

(The binary name comes from `app.json`: `native init my_app` sets `"name": "my-app"`.) Where `native dev` runs a Debug build to arm hot reload, `native build` produces an optimized ReleaseFast binary. The TypeScript core compiles to native code inside it — no JS engine, no interpreter. From there, [Packaging](/docs/packaging) turns it into a distributable app bundle with `native package`.

## Escape hatch: own the build

If the app outgrows the managed graph — extra build steps, custom sources — run `native eject` once. It writes a `build.zig`/`build.zig.zon` you own into the app and never touches them again; `native dev|build|test` keep working, now driving your files through `zig build`. See the [CLI](/docs/cli) reference.

## Next steps

- [App Model](/docs/app-model) — the model/message/update loop, wiring, and hot reload
- [TypeScript Cores](/docs/typescript) — the app-core subset, effects, subscriptions, and the dev loop in depth
- [Native UI](/docs/native-ui) — every element, attribute, and pattern in the markup
- [Components](/docs/components) — the component catalog
- [State & Data Flow](/docs/state) — derive-don't-store, bindings, and text editing state
- [Zig 0.16 Notes](/docs/zig) — the standard-library idioms this SDK uses, mapped from the compile errors older Zig habits produce
- [Examples](https://github.com/vercel-labs/native/tree/main/examples) — complete apps in the repository, from a calculator to a native shell
- [Web Content](/docs/frontend) — the secondary path for apps that embed an existing web frontend
- [Platform Support](/docs/platform-support) — what each host supports today

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Record Store
url: "https://native-sdk.dev/docs/record-store"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The record store persists independent byte records without giving `update` a database handle or making the app own a file format. It is the right fit for caches, message history, and document-sized values that grow or change one record at a time. Declare the build capability in `app.json`:

```json title="app.json"
{
  "capabilities": ["store"]
}
```

`"store"` links the shared SQLite engine and opens one engine-owned `store.db` in the app-data directory. Apps name keys, never paths or SQL. The common desktop runner and the iOS and Android hosts install that data directory before the first app effect. Apps without either storage capability do not link SQLite, and `native check` warns when the `"store"` declaration and `Cmd.store.*` calls disagree. The relational tier's `"sqlite"` capability selects the same engine object, so an app declaring both still links SQLite once.

## A saved draft

Every operation is a command. The committed model changes first; the result returns later as an ordinary `Msg`.

```ts title="src/core.ts"
import { Cmd } from "@native-sdk/core";

export interface Model {
  readonly draft: Uint8Array;
  readonly loaded: boolean;
}

export type Msg =
  | { readonly kind: "load" }
  | { readonly kind: "loaded"; readonly result: Uint8Array }
  | { readonly kind: "edited"; readonly draft: Uint8Array }
  | { readonly kind: "saved" }
  | { readonly kind: "store_failed"; readonly reason: Uint8Array };

export const viewUnbound = ["loaded", "saved", "store_failed"] as const;

export function initialModel(): Model {
  return { draft: new Uint8Array(0), loaded: false };
}

export function update(model: Model, msg: Msg): Model | [Model, Cmd<Msg>] {
  switch (msg.kind) {
    case "load":
      return [model, Cmd.store.get("draft/current", {
        key: "load-draft", ok: "loaded", err: "store_failed",
      })];
    case "loaded":
      // A get result starts with 1 for a hit and 0 for a miss. The value
      // follows the hit byte, so an empty value is distinct from absence.
      return msg.result[0] === 1
        ? { draft: msg.result.subarray(1), loaded: true }
        : { ...model, loaded: true };
    case "edited":
      return [{ ...model, draft: msg.draft }, Cmd.store.set(
        "draft/current",
        msg.draft,
        { key: "save-draft", ok: "saved", err: "store_failed" },
      )];
    case "saved":
    case "store_failed":
      return model;
  }
}
```

Reissuing the same route `key` replaces the older in-flight operation, and `Cmd.cancel(key)` cancels it silently. Distinct commands issued in one commit are performed in command-stream order: a synchronous read waits for writes that precede it. A get in a later commit also observes an earlier successful set.

## Zig-core parity

Zig cores use the same runtime-owned database and result envelope. The common app runner installs the binding before the app's first effect; no Zig entry point resolves a path or opens SQLite.

```zig
const Msg = union(enum) {
    store_result: native_sdk.EffectHostResult,
};
const Effects = native_sdk.Effects(Msg);

fn loadDraft(fx: *Effects) void {
    fx.storeGet(.{
        .key = 1,
        .record_key = "draft/current",
        .on_result = Effects.hostMsg(.store_result),
    });
}
```

`Effects.storeSet`, `storeGet`, `storeDelete`, `storeScan`, and `storeSetMany` mirror the TypeScript operations. Their `EffectHostResult` carries `key`, `ok`, and `bytes`; get and scan use exactly the framing described below.

## Operations and bounds

<table>
  <thead>
    <tr>
      <th>
        Command
      </th>

      <th>
        Behavior
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `Cmd.store.set(key, bytes, route)`
      </td>

      <td>
        Insert or replace one value. Keys are non-empty UTF-8 up to 512 bytes; values are at most 1 MiB.
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.store.get(key, route)`
      </td>

      <td>
        Return

        `[1][value...]`

        for a hit or

        `[0]`

        for a miss through the ok arm.
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.store.delete(key, route)`
      </td>

      <td>
        Delete one value. A missing key succeeds.
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.store.scan(prefix, options, route)`
      </td>

      <td>
        Return a byte-lexicographic prefix page.

        `limit`

        defaults to 100 and is capped at 256; pass the returned next-key bytes as

        `after`

        (a known literal key may be passed as a string).
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.store.setMany(entries, route)`
      </td>

      <td>
        Insert or replace 1–64 records atomically, with an 8 MiB encoded batch bound.
      </td>
    </tr>
  </tbody>
</table>

A scan page is little-endian framed bytes: `count u32`, then `count` repetitions of `key_length u32`, key bytes, `value_length u32`, value bytes, followed by `next_length u32` and the next-key bytes. An empty next key ends pagination. Pages stop at record boundaries; records are never truncated.

All error arms receive one closed reason as UTF-8 bytes: `io_failed`, `over_bound`, `bad_key`, `rejected`, or `busy`. Cache misses use the get ok arm because absence is an expected lookup result. `setMany` validates the entire batch before its transaction, so an invalid entry changes nothing.

## Replay and the virtual host

Store results use the ordinary effect journal. Session replay feeds the recorded result and never opens the live database. Zig full-loop tests opt into one hermetic SQLite database per harness with `TestHarness().createWithRecordStore(allocator, surface)`; it is bound before `harness.start(app)` and closed by `harness.destroy(allocator)`. `native dev --core` performs the same API against a process-local map that survives its simulated `{"restart": true}` command.

Use [Model Persistence](/docs/persistence) when the whole in-memory model is the unit you save. Use the record store when records grow independently. Use raw file effects only for user-visible files, exports, or blobs larger than the record bound; relational queries and secondary indexes belong in the SQL tier rather than this API.

The repository's [record-store example](https://github.com/vercel-labs/native/tree/main/examples/record-store) exercises all five commands from a TypeScript core and Native markup view.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: "App & Runtime"
url: "https://native-sdk.dev/docs/runtime"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

`App` and `Runtime` are the layer underneath [`UiApp`](/docs/app-model): the app describes what to run, the runtime owns the event loop, windows, native views, security checks, automation, tracing, and platform services. Most native-rendered apps never touch this layer directly — `UiApp` and the generated runner wire it for you. Reach for it when you need custom lifecycle callbacks, imperative window and view management, or a [WebView-based frontend](/docs/frontend).

## The App struct

<table>
  <thead>
    <tr>
      <th>
        Field
      </th>

      <th>
        Type
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `context`
      </td>

      <td>
        `*anyopaque`
      </td>

      <td>
        Pointer to your app state (required)
      </td>
    </tr>

    <tr>
      <td>
        `name`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        App name used in traces and automation snapshots (required)
      </td>
    </tr>

    <tr>
      <td>
        `source`
      </td>

      <td>
        `WebViewSource`
      </td>

      <td>
        Initial WebView content for compatibility startup windows; defaults to empty HTML
      </td>
    </tr>

    <tr>
      <td>
        `source_fn`
      </td>

      <td>
        `?fn(*anyopaque) !WebViewSource`
      </td>

      <td>
        Dynamic source resolver (overrides

        `source`

        when set)
      </td>
    </tr>

    <tr>
      <td>
        `scene_fn`
      </td>

      <td>
        `?fn(*anyopaque) !ShellConfig`
      </td>

      <td>
        Declarative native window and view tree; when set, startup uses the scene instead of the compatibility window list
      </td>
    </tr>

    <tr>
      <td>
        `start_fn`
      </td>

      <td>
        `?fn(*anyopaque, *Runtime) !void`
      </td>

      <td>
        Called after the runtime starts, before startup scene or source loading
      </td>
    </tr>

    <tr>
      <td>
        `event_fn`
      </td>

      <td>
        `?fn(*anyopaque, *Runtime, Event) !void`
      </td>

      <td>
        Called on every runtime event (lifecycle + commands)
      </td>
    </tr>

    <tr>
      <td>
        `stop_fn`
      </td>

      <td>
        `?fn(*anyopaque, *Runtime) !void`
      </td>

      <td>
        Called before the runtime shuts down
      </td>
    </tr>
  </tbody>
</table>

All callback fields are optional. A minimal app only needs `context` and `name`; provide `scene_fn` when startup should build a declarative native shell, and `source` or `source_fn` only when the app has WebView content.

## Startup shape

With `scene_fn`, the runtime materializes the returned `ShellConfig` as native shell windows and views — this is the native-first path, and the shape `UiApp` uses through its `scene` option. The first scene window adopts the startup native window; additional scene windows are created through the window service.

Scene windows and views are kept as resize layout bindings, so return slices backed by static or app-owned storage that lives as long as the window.

```zig
const shell_views = [_]native_sdk.ShellView{
    .{ .label = "main-canvas", .kind = .gpu_surface, .fill = true, .gpu_backend = .metal },
    .{ .label = "status", .kind = .statusbar, .edge = .bottom, .height = 28, .text = "Ready" },
};
const shell_windows = [_]native_sdk.ShellWindow{.{
    .label = "main",
    .title = "Acme",
    .width = 1100,
    .height = 760,
    .views = &shell_views,
}};
const shell_scene: native_sdk.ShellConfig = .{ .windows = &shell_windows };

fn scene(context: *anyopaque) anyerror!native_sdk.ShellConfig {
    _ = context;
    return shell_scene;
}
```

Without `scene_fn`, the runtime uses the WebView compatibility path: it loads `source` or `source_fn` into the configured startup window list.

## WebViewSource

For apps that embed web content, three constructors specify what a WebView loads:

- **`.html(content)`** -- inline HTML string, served as `zero://inline`
- **`.url(address)`** -- load a remote or local URL
- **`.assets(options)`** -- serve a local file tree through a custom origin

The assets constructor takes a `WebViewAssetSource`:

```zig
.source = native_sdk.WebViewSource.assets(.{
    .root_path = "dist",
    .entry = "index.html",      // default
    .origin = "zero://app",     // default
    .spa_fallback = true,       // default
}),
```

<table>
  <thead>
    <tr>
      <th>
        Field
      </th>

      <th>
        Default
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `root_path`
      </td>

      <td>
        required
      </td>

      <td>
        Path to the directory containing frontend assets
      </td>
    </tr>

    <tr>
      <td>
        `entry`
      </td>

      <td>
        `"index.html"`
      </td>

      <td>
        HTML entry point within the root path
      </td>
    </tr>

    <tr>
      <td>
        `origin`
      </td>

      <td>
        `"zero://app"`
      </td>

      <td>
        Origin used for asset URLs
      </td>
    </tr>

    <tr>
      <td>
        `spa_fallback`
      </td>

      <td>
        `true`
      </td>

      <td>
        Serve entry for unknown routes (SPA mode)
      </td>
    </tr>
  </tbody>
</table>

See [Frontend Projects](/docs/frontend) for the dev-server/bundled-assets workflow.

## Lifecycle events

The runtime dispatches `LifecycleEvent` values through your `event_fn`:

- **`start`** -- the app runtime has started and startup scene or source loading is about to run
- **`activate`** -- the app became the active foreground app
- **`deactivate`** -- the app resigned active foreground status
- **`frame`** -- a frame has been requested (for animations or state updates)
- **`stop`** -- the app is shutting down

Native file drops dispatch `Event.files_dropped` to `event_fn`; `UiApp` maps them through `Options.on_drop`, and TypeScript cores export `dropMsg(drop: FileDropEvent)` (see [Native UI: File drops](/docs/native-ui#file-drops)). Apps with WebView content also receive `app:activate`, `app:deactivate`, and `drop:files` on each trusted `window.zero` instance:

```ts
window.zero.on("drop:files", (event) => {
  console.log(event.paths);
});
```

## The runner pattern

The generated `src/runner.zig` wires the runtime with platform services:

1. Selects the platform (macOS, Linux, Windows, or null for headless tests)
2. Sets up trace sinks (stdout + file) via `FanoutTraceSink`
3. Installs panic capture so crashes write `last-panic.txt`
4. Initializes window state persistence from `windows.zon`
5. Creates the `Runtime` with all options and calls `runtime.run(app)`

```zig
var runtime = native_sdk.Runtime.init(.{
    .platform = my_platform,
    .trace_sink = fanout.sink(),
    .security = .{
        .permissions = &app_permissions,
        .navigation = .{ .allowed_origins = &.{ "zero://app" } },
    },
    .window_state_store = state_store,
    .automation = if (build_options.automation) automation_server else null,
});
try runtime.run(my_app.app());
```

## RuntimeOptions

<table>
  <thead>
    <tr>
      <th>
        Field
      </th>

      <th>
        Type
      </th>

      <th>
        Default
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `platform`
      </td>

      <td>
        `Platform`
      </td>

      <td>
        required
      </td>

      <td>
        Platform abstraction (macOS, Linux, or NullPlatform)
      </td>
    </tr>

    <tr>
      <td>
        `trace_sink`
      </td>

      <td>
        `?trace.Sink`
      </td>

      <td>
        `null`
      </td>

      <td>
        Destination for structured trace records
      </td>
    </tr>

    <tr>
      <td>
        `log_path`
      </td>

      <td>
        `?[]const u8`
      </td>

      <td>
        `null`
      </td>

      <td>
        Path for persistent log file
      </td>
    </tr>

    <tr>
      <td>
        `extensions`
      </td>

      <td>
        `?ModuleRegistry`
      </td>

      <td>
        `null`
      </td>

      <td>
        Extension modules with lifecycle hooks
      </td>
    </tr>

    <tr>
      <td>
        `bridge`
      </td>

      <td>
        `?BridgeDispatcher`
      </td>

      <td>
        `null`
      </td>

      <td>
        App-defined bridge commands and handlers (WebView apps; see

        <a href="/docs/bridge">Bridge</a>

        )
      </td>
    </tr>

    <tr>
      <td>
        `builtin_bridge`
      </td>

      <td>
        `BridgePolicy`
      </td>

      <td>
        <code>.</code>
      </td>

      <td>
        Policy for built-in bridge commands (dialogs, windows)
      </td>
    </tr>

    <tr>
      <td>
        `security`
      </td>

      <td>
        `SecurityPolicy`
      </td>

      <td>
        <code>.</code>
      </td>

      <td>
        Navigation allowlist, external links, permissions
      </td>
    </tr>

    <tr>
      <td>
        `automation`
      </td>

      <td>
        `?automation.Server`
      </td>

      <td>
        `null`
      </td>

      <td>
        File-based automation server for testing
      </td>
    </tr>

    <tr>
      <td>
        `window_state_store`
      </td>

      <td>
        `?window_state.Store`
      </td>

      <td>
        `null`
      </td>

      <td>
        Persistent window geometry and state
      </td>
    </tr>

    <tr>
      <td>
        `js_window_api`
      </td>

      <td>
        `bool`
      </td>

      <td>
        `false`
      </td>

      <td>
        Expose built-in

        `window.zero`

        helper namespaces for trusted app chrome. Command helpers require origin and

        `command`

        checks, view helpers require origin and

        `view`

        checks, and window, WebView, and platform helpers require origin and

        `window`

        checks. Dialog, OS, clipboard, and credential helpers still require explicit

        `builtin_bridge`

        policy.
      </td>
    </tr>
  </tbody>
</table>

## Runtime methods

<table>
  <thead>
    <tr>
      <th>
        Method
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `init(options) Runtime`
      </td>

      <td>
        Create a runtime
      </td>
    </tr>

    <tr>
      <td>
        `run(app) !void`
      </td>

      <td>
        Enter the platform event loop
      </td>
    </tr>

    <tr>
      <td>
        `createWindow(options) !WindowInfo`
      </td>

      <td>
        Open a new window
      </td>
    </tr>

    <tr>
      <td>
        `listWindows() []WindowInfo`
      </td>

      <td>
        List open windows
      </td>
    </tr>

    <tr>
      <td>
        `focusWindow(id) !void`
      </td>

      <td>
        Bring a window to front
      </td>
    </tr>

    <tr>
      <td>
        `closeWindow(id) !void`
      </td>

      <td>
        Close a window
      </td>
    </tr>

    <tr>
      <td>
        `showWindow(id) !void`
      </td>

      <td>
        Un-hide and activate a window (the counterpart to a

        `close_policy = "hide"`

        hide; also restores a minimized window)
      </td>
    </tr>

    <tr>
      <td>
        `quitApp() !void`
      </td>

      <td>
        Quit gracefully through the same shutdown path a last-window close takes
      </td>
    </tr>

    <tr>
      <td>
        `invalidate()`
      </td>

      <td>
        Request a redraw
      </td>
    </tr>

    <tr>
      <td>
        `invalidateFor(reason, dirty_region)`
      </td>

      <td>
        Request a redraw with reason and optional dirty region
      </td>
    </tr>

    <tr>
      <td>
        `frameDiagnostics() FrameDiagnostics`
      </td>

      <td>
        Return stats from the last frame
      </td>
    </tr>

    <tr>
      <td>
        `setGpuSurfaceInputLatencyBudget(window_id, label, budget_ns)`
      </td>

      <td>
        Set the input-to-frame latency budget reported by GPU surface diagnostics
      </td>
    </tr>

    <tr>
      <td>
        `dispatchEvent(event)`
      </td>

      <td>
        Inject a synthetic event
      </td>
    </tr>

    <tr>
      <td>
        `dispatchPlatformEvent(app, event)`
      </td>

      <td>
        Forward a platform event
      </td>
    </tr>

    <tr>
      <td>
        `automationSnapshot()`
      </td>

      <td>
        Write state to automation directory
      </td>
    </tr>
  </tbody>
</table>

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Security
url: "https://native-sdk.dev/docs/security"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Declare `permissions` and `capabilities` in `app.json` (or a supported `app.zon` manifest). These control access to native services. Apps that [embed web content](/docs/frontend) also configure bridge command policies, allowed origins, and navigation rules. A native-rendered app without a WebView uses the permissions and capabilities section below.

## Permissions and capabilities

`capabilities` describe broad features an app uses. `permissions` are the runtime grants checked before native commands run.

```zig
.permissions = .{ "command", "view", "dialog", "window", "filesystem" },
.capabilities = .{ "webview", "js_bridge", "native_views", "dialog", "filesystem" },
```

### Available permissions

<table>
  <thead>
    <tr>
      <th>
        Permission
      </th>

      <th>
        Grants
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `window`
      </td>

      <td>
        Window create/focus/close operations and layered WebView management
      </td>
    </tr>

    <tr>
      <td>
        `command`
      </td>

      <td>
        App command routing from trusted WebView bridge calls
      </td>
    </tr>

    <tr>
      <td>
        `view`
      </td>

      <td>
        Generic native view create/list/update/focus/close operations
      </td>
    </tr>

    <tr>
      <td>
        `dialog`
      </td>

      <td>
        Native file and message dialogs from explicit builtin bridge policies
      </td>
    </tr>

    <tr>
      <td>
        `filesystem`
      </td>

      <td>
        Raw file effects and bridge commands outside this app's resolved data/config/cache/state/logs/temp roots; symlinks are resolved before the runtime check
      </td>
    </tr>

    <tr>
      <td>
        `clipboard`
      </td>

      <td>
        Clipboard read/write
      </td>
    </tr>

    <tr>
      <td>
        `credentials`
      </td>

      <td>
        Credential store read/write/delete operations
      </td>
    </tr>

    <tr>
      <td>
        `network`
      </td>

      <td>
        Network requests from native code
      </td>
    </tr>

    <tr>
      <td>
        `camera`
      </td>

      <td>
        Camera access
      </td>
    </tr>

    <tr>
      <td>
        `microphone`
      </td>

      <td>
        Microphone access
      </td>
    </tr>

    <tr>
      <td>
        `system_audio`
      </td>

      <td>
        System-output audio capture
      </td>
    </tr>

    <tr>
      <td>
        `location`
      </td>

      <td>
        Location services
      </td>
    </tr>

    <tr>
      <td>
        `notifications`
      </td>

      <td>
        System notifications
      </td>
    </tr>
  </tbody>
</table>

Custom permissions use reverse-DNS names (e.g. `com.example.my-permission`). Use the smallest set that covers your app.

### Available capabilities

<table>
  <thead>
    <tr>
      <th>
        Capability
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `webview`
      </td>

      <td>
        WebView rendering
      </td>
    </tr>

    <tr>
      <td>
        `js_bridge`
      </td>

      <td>
        JavaScript bridge
      </td>
    </tr>

    <tr>
      <td>
        `native_module`
      </td>

      <td>
        Native Zig extension modules
      </td>
    </tr>

    <tr>
      <td>
        `native_views`
      </td>

      <td>
        Native shell views, chrome, controls, and utility panels
      </td>
    </tr>

    <tr>
      <td>
        `menus`
      </td>

      <td>
        Native app and window menus
      </td>
    </tr>

    <tr>
      <td>
        `shortcuts`
      </td>

      <td>
        Keyboard shortcut registration
      </td>
    </tr>

    <tr>
      <td>
        `tray`
      </td>

      <td>
        System tray integration
      </td>
    </tr>

    <tr>
      <td>
        `filesystem`
      </td>

      <td>
        File system access
      </td>
    </tr>

    <tr>
      <td>
        `network`
      </td>

      <td>
        Network access
      </td>
    </tr>

    <tr>
      <td>
        `notifications`
      </td>

      <td>
        System notification access
      </td>
    </tr>

    <tr>
      <td>
        `dialog`
      </td>

      <td>
        Native file and message dialog access
      </td>
    </tr>

    <tr>
      <td>
        `clipboard`
      </td>

      <td>
        Clipboard access
      </td>
    </tr>

    <tr>
      <td>
        `credentials`
      </td>

      <td>
        Credential store access
      </td>
    </tr>

    <tr>
      <td>
        `open_url`
      </td>

      <td>
        Open URLs with the system browser
      </td>
    </tr>

    <tr>
      <td>
        `reveal_path`
      </td>

      <td>
        Reveal local paths in the platform file manager
      </td>
    </tr>

    <tr>
      <td>
        `recent_documents`
      </td>

      <td>
        Platform recent document registration
      </td>
    </tr>

    <tr>
      <td>
        `file_drops`
      </td>

      <td>
        Native file drop events
      </td>
    </tr>

    <tr>
      <td>
        `app_activation_events`
      </td>

      <td>
        App activate and deactivate lifecycle events
      </td>
    </tr>

    <tr>
      <td>
        `file_associations`
      </td>

      <td>
        Package metadata for file type registration
      </td>
    </tr>

    <tr>
      <td>
        `url_schemes`
      </td>

      <td>
        Package metadata for URL scheme registration
      </td>
    </tr>
  </tbody>
</table>

## Native commands

Native bridge commands are default-deny. A command must be registered by native code **and** allowed by policy before the runtime invokes it.

```zig
.bridge = .{
    .commands = .{
        .{
            .name = "native.ping",
            .origins = .{ "zero://app" },
        },
        .{
            .name = "native-sdk.window.create",
            .permissions = .{ "window" },
            .origins = .{ "zero://app" },
        },
    },
},
```

Prefer exact origins over `"*"`. Use `"*"` only for local development or commands that do not expose native state.

## Builtin bridge policy

The Native SDK provides built-in commands for app command routing (`native-sdk.command.*`), windows (`native-sdk.window.*`), generic native views (`native-sdk.view.*`), layered WebViews (`native-sdk.webview.*`), platform support queries (`native-sdk.platform.*`), dialogs (`native-sdk.dialog.*`), selected OS capabilities (`native-sdk.os.*`), clipboard access (`native-sdk.clipboard.*`), and credential storage (`native-sdk.credentials.*`). These are controlled separately from app-defined commands via the `builtin_bridge` field in `RuntimeOptions`.

`js_window_api` exposes the JavaScript command, window, view, WebView, and platform support helpers, but it does not bypass security. Command routing (`native-sdk.command.invoke`) must come from an allowed origin and have the `command` permission when runtime permissions are configured. Generic native view commands (`native-sdk.view.create`, `list`, `update`, `setFrame`, `setVisible`, `focus`, `focusNext`, `focusPrevious`, `close`) require the `view` permission. Platform support queries, window commands (`native-sdk.window.list`, `create`, `focus`, `close`), and WebView commands (`native-sdk.webview.create`, `list`, `setFrame`, `navigate`, `setZoom`, `setLayer`, `close`) require the `window` permission. The legacy `window` grant is still accepted for command and view helpers for compatibility. View and WebView commands can only target the window that sent the bridge message. WebView URLs must also be allowed by `security.navigation.allowed_origins`, and child WebViews receive `window.zero` only when created with `bridge: true`.

For broader control, use an explicit `builtin_bridge` policy. When you choose this path, list every built-in command your app calls. Dialog commands (`native-sdk.dialog.openFile`, `saveFile`, `showMessage`), OS commands (`native-sdk.os.openUrl`, `showNotification`, `revealPath`, `addRecentDocument`, `clearRecentDocuments`), clipboard commands (`native-sdk.clipboard.readText`, `writeText`, `read`, `write`), and credential commands (`native-sdk.credentials.set`, `get`, `delete`) are **always default-deny** and require an explicit `builtin_bridge` policy with the command listed. `native-sdk.os.openUrl` also requires `security.navigation.external_links` to allow the target URL. A `showNotification` request with `actionLabel` / `actionCommand` additionally requires `native-sdk.command.invoke` and its `command` permission for the same origin:

```zig
.builtin_bridge = .{
    .enabled = true,
    .commands = &.{
        .{ .name = "native-sdk.command.invoke", .permissions = .{ "command" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.platform.supports", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.window.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.view.create", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.view.list", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.view.update", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.view.setFrame", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.view.setVisible", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.view.focus", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.view.focusNext", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.view.focusPrevious", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.view.close", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.webview.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.webview.list", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.webview.setFrame", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.webview.navigate", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.webview.setZoom", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.webview.setLayer", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.webview.close", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.dialog.openFile", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.dialog.saveFile", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.dialog.showMessage", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.openUrl", .permissions = .{ "network" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.showNotification", .permissions = .{ "notifications" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.revealPath", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.addRecentDocument", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.clearRecentDocuments", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.clipboard.readText", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.clipboard.writeText", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.clipboard.read", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.clipboard.write", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.credentials.set", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.credentials.get", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.credentials.delete", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } },
    },
},
```

## Bridge error codes

When a bridge call fails, the JavaScript promise rejects with an error containing a `code` field:

<table>
  <thead>
    <tr>
      <th>
        Code
      </th>

      <th>
        Cause
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `invalid_request`
      </td>

      <td>
        Malformed input, unsupported built-in operation, denied navigation URL, missing window/WebView, duplicate or reserved WebView label, or another caller-fixable request problem
      </td>
    </tr>

    <tr>
      <td>
        `unknown_command`
      </td>

      <td>
        No handler registered for this command
      </td>
    </tr>

    <tr>
      <td>
        `permission_denied`
      </td>

      <td>
        Origin or permission check failed
      </td>
    </tr>

    <tr>
      <td>
        `handler_failed`
      </td>

      <td>
        Handler returned an error
      </td>
    </tr>

    <tr>
      <td>
        `payload_too_large`
      </td>

      <td>
        Message exceeds 16 KiB limit
      </td>
    </tr>

    <tr>
      <td>
        `internal_error`
      </td>

      <td>
        Unexpected runtime error
      </td>
    </tr>
  </tbody>
</table>

Handle errors in JavaScript:

```javascript
try {
  const result = await window.zero.invoke("native.ping", {});
} catch (error) {
  console.error(error.code, error.message);
}
```

## Navigation policy

Main-frame navigation is allowlisted. Packaged assets normally use `zero://app`, inline examples use `zero://inline`, and dev servers should list their exact local origin.

```zig
.security = .{
    .navigation = .{
        .allowed_origins = .{
            "zero://app",
            "zero://inline",
            "http://127.0.0.1:5173",
        },
    },
},
```

Unknown main-frame navigations are blocked unless the external-link policy explicitly handles them.

## External links

External links are denied by default. To open links in the system browser, opt in and list allowed URLs:

```zig
.security = .{
    .navigation = .{
        .external_links = .{
            .action = "open_system_browser",
            .allowed_urls = .{ "https://example.com/docs/*" },
        },
    },
},
```

Do not allow broad external patterns for pages that can be influenced by remote content.

Each entry is an exact HTTP(S) URL, `*` to allow every HTTP(S) URL, or an HTTP(S) prefix ending in a single `*` after the host and a `/`. A `*` in the hostname or anywhere before the final character is invalid; `native check` rejects it. For example, `https://example.com/docs/*` allows paths under `/docs/`, but not `https://example.com/docs` or the bare origin. List those URLs separately if needed.

The same policy gates `Cmd.openExternalUrl(...)`, `runtime.openExternalUrl(...)`, and `window.zero.os.openUrl(...)`. A bridge grant for `native-sdk.os.openUrl` is not enough by itself; the URL must also match `external_links.allowed_urls`. A denied `Cmd.openExternalUrl(...)` emits a warning in the app's stderr output; `window.zero.os.openUrl(...)` returns an error to the caller.

## CSP guidance

For packaged assets, start with a strict Content Security Policy:

```html
<meta http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'">
```

For inline Zig examples that embed scripts or styles, add only the minimum inline allowances:

```html
<meta http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'">
```

For dev servers, extend `connect-src` only to the local dev origin and WebSocket endpoint required by the framework. Keep production CSP separate from development CSP.

## Security model summary

<table>
  <thead>
    <tr>
      <th>
        Layer
      </th>

      <th>
        Default
      </th>

      <th>
        Opt-in
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        App bridge commands
      </td>

      <td>
        Denied
      </td>

      <td>
        Per-command policy with origin and permission checks
      </td>
    </tr>

    <tr>
      <td>
        Builtin bridge (commands, windows, views, and WebViews)
      </td>

      <td>
        Denied unless

        `js_window_api`

        or explicit policy allows the helper and origin/permission checks pass
      </td>

      <td>
        `command`

        ,

        `view`

        , or

        `window`

        permission plus exact allowed origins
      </td>
    </tr>

    <tr>
      <td>
        Builtin bridge (dialogs and OS capabilities)
      </td>

      <td>
        Denied
      </td>

      <td>
        Explicit

        `builtin_bridge`

        policy with matching permissions required
      </td>
    </tr>

    <tr>
      <td>
        Navigation
      </td>

      <td>
        Blocked
      </td>

      <td>
        Allowlisted origins
      </td>
    </tr>

    <tr>
      <td>
        External links
      </td>

      <td>
        Denied
      </td>

      <td>
        Explicit action + URL prefix list
      </td>
    </tr>

    <tr>
      <td>
        Permissions
      </td>

      <td>
        None granted
      </td>

      <td>
        Declared in the app manifest and checked at runtime
      </td>
    </tr>

    <tr>
      <td>
        CSP
      </td>

      <td>
        Not enforced by the Native SDK
      </td>

      <td>
        Set in your HTML

        `<meta>`

        tag
      </td>
    </tr>
  </tbody>
</table>

The goal is defense in depth: even if a command is registered in Zig, it won't execute unless the policy allows it from the requesting origin with the required permissions.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Agent Skills
url: "https://native-sdk.dev/docs/skills"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Native SDK ships its agent guidance as skills: curated, versioned `SKILL.md` documents that the CLI delivers to coding agents.

Use the installed CLI to retrieve guidance for that SDK version.

## Install for your agent

One command sets an agent up:

```sh
npx skills add vercel-labs/native
```

It installs the Native SDK discovery skill into your agent's skills directory: a compact entry point that orients the agent and teaches it to load the full, version-matched skill content from the installed CLI with the `native skills` commands below. Install once — because the deep guidance is always read from the CLI, the agent stays current with whatever toolkit version each project uses.

## Getting skills from the CLI

The CLI itself serves the full skill content. This is the layer the installed discovery skill reads from, and the direct path when you want to print a skill or deliver one by hand — it needs nothing beyond the installed CLI, so it also works offline:

```sh
native skills list                    # list built-in skills
native skills get core                # print a skill
native skills get native-ui           # default app views and app loop
native skills get ts-core             # default TypeScript app core
native skills get ts-services         # compiled TypeScript services
native skills get core --full         # include its reference files
native skills get --all [--full]      # print every skill
```

For a normal app created by `native init`, give the agent both `native-ui` and `ts-core`: TypeScript + Native markup is the primary authoring path. Load `ts-services` for imperative TypeScript under `src/services/`. Load `core --full` when the task reaches shared runtime wiring, WebViews, packaging, or native capabilities; load `zig` for an existing Zig-core app or toolkit-extension work.

`skills get` prints the skill to stdout, so delivering one to an agent is a single redirect into wherever your agent loads skills from:

```sh
mkdir -p .claude/skills/native-ui
native skills get native-ui > .claude/skills/native-ui/SKILL.md
```

`native init` does not copy skills into the workspace — delivery is always explicit, so the skill in the workspace is exactly what the installed CLI serves. The repository's own eval harness gives fresh agents the `native-ui` skill this same way before grading what they build.

## What ships

<dl>
  <dt>
    `core`
  </dt>

  <dd>
    The shared foundation and task router: it establishes TypeScript + Native markup as the default, then covers project structure, app manifests, lower-level App and Runtime patterns, frontend integration, web engines, the JavaScript bridge, permissions, windows, WebViews, dialogs, packaging, debugging, and testing.

    `--full`

    appends its five reference files.
  </dd>

  <dt>
    `native-ui`
  </dt>

  <dd>
    The view half of primary app authoring:

    `.native`

    markup views, bindings and message dispatch, the

    `Model`

    /

    `Msg`

    /

    `update`

    loop, testing markup views, hot reload, and verification through the automation harness.
  </dd>

  <dt>
    `ts-core`
  </dt>

  <dd>
    The logic half of primary app authoring:

    <a href="/docs/typescript">TypeScript app cores</a>

    , the app-core subset, every checker rule by ID with its idiomatic fix, text-is-bytes, the full

    `Cmd`

    /

    `Sub`

    effect vocabulary, and the node dev loop.
  </dd>

  <dt>
    `ts-services`
  </dt>

  <dd>
    Compiled

    <a href="/docs/typescript/services">TypeScript services</a>

    : typed requests and results, vendored npm packages, streaming, cancellation, and service authority.
  </dd>

  <dt>
    `automation`
  </dt>

  <dd>
    Driving and verifying a running app through

    `native automate`

    : snapshots, readiness waits, assertions, bridge round-trips, and smoke tests.
  </dd>

  <dt>
    `zig`
  </dt>

  <dd>
    Zig 0.16 idioms indexed by compile error, for fixing code written against older Zig:

    `std.Io`

    file IO and writers,

    `ArrayList`

    , spawning, clocks, formatting, and

    `build.zig`

    shapes, each as the SDK writes them. The human version is

    <a href="/docs/zig">Zig 0.16 Notes</a>

    .
  </dd>
</dl>

## Skills in the agent workflow

Use skills for API and authoring guidance, the [CLI](/docs/cli) for checks and builds, and [automation](/docs/automation) to inspect and test a running app.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Relational SQLite
url: "https://native-sdk.dev/docs/sqlite"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The relational tier gives a model core an engine-owned SQLite database while keeping `update` pure. Add the capability in `app.json`:

```json title="app.json"
{
  "capabilities": ["sqlite"]
}
```

The runner opens `app.db` in the app-data directory, enables WAL and foreign keys, applies pending migrations, and keeps the path private. Apps that declare neither `"sqlite"` nor `"store"` do not link SQLite. The record store uses a separate `store.db`.

## Make the schema append-only

Migration files under `src/schema/` are the source of truth. Names are contiguous and monotonic:

```text
src/schema/0001_init.sql
src/schema/0002_add_tags.sql
```

Create the next file with:

```sh
native db new-migration add-tags
```

Use `STRICT` for ordinary generated-query tables so SQLite's storage classes agree with generated TypeScript types:

```sql title="src/schema/0001_init.sql"
CREATE TABLE folder (
  id INTEGER PRIMARY KEY,
  name TEXT NOT NULL UNIQUE
) STRICT;

CREATE TABLE note (
  id INTEGER PRIMARY KEY,
  folder_id INTEGER NOT NULL REFERENCES folder(id),
  title TEXT NOT NULL
) STRICT;
```

`native check` applies the complete chain to a real in-memory SQLite database. It rejects gaps, edited published migrations, invalid SQLite, non-STRICT tables, and a schema change that is not represented by a new version. The accepted hashes are written to `src/schema/migrations.lock.json`; commit that file with the migrations so append-only validation has the same authority in every checkout and in CI. At launch the runtime compares the chain with `PRAGMA user_version` and applies all pending files in one transaction. A migration failure refuses the database open; a database newer than the binary is `version_unknown` and is never repaired or downgraded automatically.

`native db status` compares source and installed versions. `native db reset --yes` deletes the development `app.db`, WAL, and shared-memory files; the next launch reapplies the chain. Reset never accepts an arbitrary path.

## Declare checked queries

Put named statements in `src/queries.sql`:

```sql title="src/queries.sql"
-- name: notesInFolder :live
SELECT n.id, n.title
FROM note AS n
WHERE n.folder_id = :folder
ORDER BY n.id DESC;

-- name: moveNote :exec
UPDATE note SET folder_id = :to WHERE id = :id;
```

`native check` asks real SQLite to prepare every statement against the migrated schema. Missing tables or columns, invalid SQL, wrong read/write declarations, parameter mistakes, and invalid result shapes are reported at the `.sql` source with NS14xx diagnostics.

The accepted schema generates a flat API in `@native-sdk/core`:

- `Cmd.qNotesInFolder(params, route)` returns typed row pages.
- `Cmd.qMoveNote(params)` returns a typed transaction member.
- `Cmd.qTx([statement, ...], route)` commits all generated `:exec` members atomically.
- `Sub.qNotesInFolder(key, params, route)` exists because the query is marked `:live`.
- `NotesInFolderRow`, `NotesInFolderParams`, and `decodeNotesInFolderPage(bytes)` describe and decode its result.

The `q<Name>` spelling is intentionally flat: it stays inside the ahead-of-time core subset while retaining one-to-one names from `queries.sql`.

```ts title="src/core.ts"
import {
  Cmd,
  Sub,
  decodeNotesInFolderPage,
  utf8Bytes,
} from "@native-sdk/core";

export type Msg =
  | { readonly kind: "move" }
  | { readonly kind: "rows"; readonly page: Uint8Array }
  | { readonly kind: "rows_done" }
  | { readonly kind: "wrote" }
  | { readonly kind: "db_failed"; readonly reason: Uint8Array };

// In update:
return [model, Cmd.qTx([
  Cmd.qMoveNote({ id: 7, to: 2 }),
], { key: "move-note", ok: "wrote", err: "db_failed" })];

// In the rows arm:
const rows = decodeNotesInFolderPage(msg.page);

// In subscriptions(model):
return Sub.qNotesInFolder("folder-notes", { folder: model.folderId }, {
  page: "rows",
  done: "rows_done",
  err: "db_failed",
});
```

SQLite `INTEGER` and `REAL` map to `number`; generated decoders reject an `INTEGER` outside JavaScript's exact ±(2^53−1) range. `TEXT` and `BLOB` map to `Uint8Array`, matching the core’s byte-array storage. Generated parameters inferred from a TEXT column wrap bytes automatically. Use `dbText(bytes)` when a raw query—or a parameter whose storage class cannot be inferred, such as an FTS `MATCH` term—must bind bytes as SQLite TEXT rather than BLOB. Nullability comes from the schema.

## Live queries

A `:live` query runs when subscribed and runs again after a committed transaction touches one of its generated table dependencies. The runtime combines SQLite authorizer write targets with row-update notifications until commit, so `WITHOUT ROWID` tables and truncate-optimized deletes invalidate reliably; it coalesces invalidations once per command frame and never reruns an unrelated subscription. FTS5 shadow tables are included automatically. A `:live` declaration with no table dependency is rejected because it could never refresh.

Each delivery uses the same bounded page route as a one-shot query: zero or more `page` messages followed by `done`. Keep temporary pages in the Model and replace the visible result set on `done`. Changing a subscription's key, parameters, routes, SQL, or dependencies re-arms it; omitting the key cancels it. Dependencies are table-level in this release.

Every page and terminal crosses the session journal. Pages over 64 KiB spill into the journal's content-addressed blob store. Replay never opens SQLite: recorded one-shot and live results are fed back as ordinary Msg values, including repeated live deliveries.

## Raw escape hatch and bounds

`Cmd.db.query(sql, params, route)` and `Cmd.db.exec(statements, route)` remain available. A raw query is read-only and returns pages; one raw exec commits its entire 1–64 statement array as one transaction. `native check` warns when a raw query literal could instead be declared and checked.

Parameters accept `null`, finite `number`, literal `string`, `Uint8Array`, `dbText(bytes)`, and `boolean` (integer 0/1). A query accepts at most 64 parameters and 1 MiB of parameter bytes. A transaction accepts at most 8 MiB. SQL is capped at 64 KiB per statement. Results page at 256 rows or 256 KiB and never truncate a row; one result is capped at 8,192 rows or 8 MiB and rejects whole when it crosses either bound. Add `LIMIT` and keyset pagination for larger collections.

The binary page header is `column_count u32`, `row_count u32`, then length-prefixed UTF-8 column names. Row-major values use tags `0` NULL, `1` + signed little-endian i64, `2` + little-endian f64, `3` + length-prefixed TEXT, and `4` + length-prefixed BLOB.

The database boundary stays pathless. SQLite's authorizer denies `ATTACH`, `DETACH`, `VACUUM INTO`, and writes to engine-owned lifecycle PRAGMAs. Outcomes are closed: `constraint`, `busy`, `io_failed`, `corrupt`, `misuse`, `rejected`, and `cancelled`. Query keys replace; duplicate transaction keys reject loudly so a write is never silently lost.

Zig cores use `Effects(Msg).dbQuery`, `dbExec`, `dbSubscribe`, and `dbUnsubscribe` operations over the same runtime. `native test` and `TestHarness().createWithRelationalStore` use real in-memory SQLite. See [`examples/relational-notes`](https://github.com/vercel-labs/native/tree/main/examples/relational-notes) for migrations, typed atomic writes, FTS5, page decoding, and two live queries together.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: "State & Data Flow"
url: "https://native-sdk.dev/docs/state"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Store source data in the model, such as items, the current filter, and draft text. Derive counts, filtered lists, and formatted strings from that state when rendering the view. The patterns below apply to TypeScript and Zig cores; samples show both languages where their APIs differ.

## Pick one canonical store

For small in-memory apps, the Model owns the domain data and [model persistence](/docs/persistence) snapshots it. For relational apps, [SQLite](/docs/sqlite) owns the domain data; the Model holds view state plus the row pages most recently delivered by a query. Writes go out as one atomic `Cmd.db.exec`, and reads return as later Msg values—never as a database handle or a synchronous call inside `update`. Derived UI values still come from the Model's current rows, so the rule below does not change.

Secrets are the exception: tokens and passwords never belong in the Model, even temporarily. Store them with [`Cmd.credentials.set`](/docs/capabilities), retrieve them as an effect result, and consume the returned Msg bytes immediately to construct the next command (for example, an authenticated fetch) while returning a Model that does not retain them. That keeps secrets out of model snapshots, state fingerprints, and ordinary view data. Session recording redacts successful credential reads and replay supplies only a same-length placeholder.

## Derive, don't store

A cached derivable must be re-maintained in every `update` arm and goes stale the moment one is missed; a derived function cannot go stale.

```ts
// WRONG: derived state cached in the model, maintained by hand in update()
//   readonly visibleCount: number;

// RIGHT: the model keeps the sources; exported single-model helpers derive
// per rebuild and join the binding surface ({visible_count}, {visible_cents})
export function visibleCount(model: Model): number {
  return model.expenses.filter((e) => matches(model, e)).length;
}
```

```zig
// WRONG: derived state cached in the model, maintained by hand in update()
visible_count: usize,
summary_storage: [64]u8,   // preformatted display string

// RIGHT: the model keeps the sources; methods derive per rebuild
pub fn visibleCount(model: *const Model) usize { ... }
pub fn visibleCents(model: *const Model) u64 { ... }
```

Derived numbers need no allocation — bind the methods and let text interpolation compose the line:

```html
<status-bar>{habit_count} habits · {totalDays} total days</status-bar>
```

## How bindings resolve

A path like `{h.streak}` resolves left to right, starting from the model or a `for` variable:

- Struct fields bind directly: `{habit_count}`, `{h.done}`.
- Zero-argument pub methods bind like fields: `{totalDays}` calls `pub fn totalDays(m: *const Model) usize`.
- Arena-taking scalar methods bind the same way: `{summary}` calls `pub fn summary(m: *const Model, arena: std.mem.Allocator) []const u8` — format derived display strings straight into the build arena, which lives exactly one view build. Works anywhere a scalar binding does, except inside `{a == b}` equality (compare the source fields, or bind a `bool`-returning method).
- Enums resolve to their tag name, so `{f}` renders `"active"`, `{f == filter}` compares tags, and `set_filter:{f}` coerces the tag back into an enum payload.
- `for each="name"` resolves, in order: a model field that is a slice or array, a pub array/slice declaration, a pub fn taking `(*const Model)`, or a pub fn taking `(*const Model, std.mem.Allocator)` — the allocator variant is how filtered and derived lists work.
- Item methods work too: `{h.name}` may be a field or `pub fn name(h: *const Habit) []const u8`.

Bindings are zero-argument by design. A parameterized query (the cards of column X) becomes one model function per case, or a [template](/docs/native-ui#templates) arg.

## Format at view time, in the arena

Computed strings — money, dates, percentages — are formatted into the build arena inside a derived-list function. Store amounts as integer cents; format when the view asks:

```zig
pub const VisibleExpense = struct { id: u32, date: []const u8, amount: []const u8 };

pub fn visible(model: *const Model, arena: std.mem.Allocator) []const VisibleExpense {
    const out = arena.alloc(VisibleExpense, model.expense_count) catch return &.{};
    var count: usize = 0;
    for (model.expenses[0..model.expense_count]) |*e| {
        if (!model.matches(e.*)) continue;
        out[count] = .{
            .id = e.id,
            .date = e.date(),
            .amount = std.fmt.allocPrint(arena, "${d}.{d:0>2}", .{ e.amount_cents / 100, e.amount_cents % 100 }) catch "",
        };
        count += 1;
    }
    return out[0..count];
}
```

A one-off formatted line is an arena-taking scalar function bound directly:

```zig
pub fn summary(model: *const Model, arena: std.mem.Allocator) []const u8 {
    return std.fmt.allocPrint(arena, "{d} expenses · {s} total", .{
        model.visibleCount(), formatCents(arena, model.visibleCents()),
    }) catch "";
}
```

```html
<status-bar>{summary}</status-bar>
```

The arena lives for exactly one view build, so nothing is stored and nothing goes stale. (These formatting samples are Zig: view-time formatting runs in the view tier, which a zero-config TypeScript app never writes. Inside a TypeScript core, derived display text is bytes built with `asciiBytes` templates — see [TypeScript Cores](/docs/typescript#the-subset-posture).)

For `<if test>`, prefer an explicit boolean predicate method over numeric truthiness: `test="{hasHabits}"` with `pub fn hasHabits(m: *const Model) bool` states the condition; `test="{habit_count}"` works (non-zero is truthy) but hides it.

## Text fields: the mirror pattern

The model applies every edit event and is the source of truth; the runtime keeps caret and selection while your source text matches, and a source-side change (like clearing on submit) wins:

```html
<text-field text="{draft}" placeholder="New task…" on-input="draft_edit" on-submit="add" grow="1" />
```

```ts
// model field: the bytes the field renders; markup binds {draft}
//   readonly draft: Uint8Array;
// in update (msg.edit is the TextInputEvent mirror union your core declares):
case "draft_edit":
  return { ...model, draft: applyEdit(model.draft, msg.edit) }; // mirror every edit
case "add":
  return { ...withNewTask(model), draft: new Uint8Array(0) };   // clearing the source clears the field
```

```zig
draft_buffer: canvas.TextBuffer(64) = .{},            // model field: text + selection + composition
pub fn draft(model: *const Model) []const u8 {        // the fn the markup's text= binds
    return model.draft_buffer.text();
}
// in update:
.draft_edit => |edit| model.draft_buffer.apply(edit), // mirror every edit
.add => { model.addTask(model.draft_buffer.text()); model.draft_buffer.clear(); },  // clearing the source clears the field
```

`on-input` names a `Msg` variant carrying the text-input event. In a TypeScript core, import `TextInputEvent` and `applyTextInputEvent` from `@native-sdk/core/text` to apply edits to draft bytes. In a Zig core the payload is `canvas.TextInputEvent` and `canvas.TextBuffer` applies each event and holds the text — the markup binds the function (`text="{draft}"`), never the buffer: binding a `TextBuffer` model field directly is a teaching error pointing at this pattern. The runtime also handles clipboard and selection: the runtime owns cmd/ctrl+C/X/V in editable text, cut and paste arrive as ordinary edit events, and those edits use the same reducer.

## Echo runtime-applied values back

Some state is applied by the runtime first — a scroll offset, a split-pane fraction — and delivered to `update` after the fact. The rule is the same everywhere: **the Msg carries the value the runtime already applied; store it in the model and echo it back through the element's `value`.** The echoed source value equals the runtime's, so the reconcile treats it as unchanged and rebuilds never fight live interaction.

```html
<split value="{sidebar_split}" on-resize="sidebar_resized">
  <column min-width="150">…</column>
  <column min-width="280">…</column>
</split>
```

```ts
case "sidebar_resized":
  return { ...model, sidebarSplit: msg.fraction };
```

```zig
.sidebar_resized => |fraction| model.sidebar_split = fraction,
```

`on-scroll` uses `ScrollState` from `@native-sdk/core/events` in TypeScript or `canvas.ScrollState` in Zig. Store `offsetY` (Zig: `offset_y`) and bind it to `value`; use `offsetX` (Zig: `offset_x`) with `value-x` for horizontal scrolling. See [Native UI: Messages](/docs/native-ui#messages).

## Time: a testable seam

In a TypeScript core this is settled by rule: `update` is deterministic, and time arrives as message payloads (`Cmd.now`, `Cmd.delay`, `Sub.timer` — see [TypeScript Cores](/docs/typescript#effects-are-cmd-data)). The seam below is the Zig core's equivalent. Zig 0.16 puts `std.time` behind `std.Io`, which `update` never sees — do not call `clock_gettime` yourself. The facade owns the clocks:

```zig
native_sdk.nowMs()                  // wall ms since the Unix epoch (i64) — timestamps
native_sdk.nowNanoseconds()         // wall ns (i128)
native_sdk.monotonicMs()            // duration clock (u64, never goes backwards)
native_sdk.monotonicNanoseconds()   // subtract two reads for an elapsed time
```

Time-dependent logic should hold the seam in the model instead of calling the free functions, so tests stay deterministic:

```zig
pub const Model = struct { clock: native_sdk.Clock = .system, ... };
// in update:
.step_started => model.entry.started_ms = model.clock.wallMs(),

// in tests:
var test_clock: native_sdk.TestClock = .{};
model.clock = test_clock.clock();
test_clock.advanceMs(1500);
```

Wall time answers "what time is it?" (it jumps with OS clock adjustments); monotonic time answers "how long did it take?". Don't subtract wall timestamps for durations.

## Async state

Anything that leaves the process — subprocesses, HTTP, files, timers — flows through the effects channel and back into `update` as ordinary messages, so the model remains the single source of truth even for in-flight work. Keep the in-flight marker (`loading: bool`, an effect key) in the model like any other state. See [Native UI: Effects](/docs/native-ui#effects).

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Terminal
url: "https://native-sdk.dev/docs/terminal"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Run a real shell on a pseudo-terminal, stream its output into a terminal emulator, and paint the grid as **real text** on the canvas. The pty vocabulary is effects-as-data all the way down: your core names a model-owned pty key, a command, and a grid size; the host opens the pty, forks the child onto it, and streams coalesced output back as Msgs. Nothing in `update` ever touches a file descriptor or forks a process.

Recorded sessions replay the journaled output batches and exit event without starting a shell.

The terminal state itself — cursor, styles, scrollback, wrapping, reflow, selection — is owned by [libghostty-vt](https://github.com/ghostty-org/ghostty), Ghostty's extracted VT core, consumed as the `ghostty-vt` Zig module. The toolkit never parses escape sequences; it owns the transport (the pty) and the pixels (the canvas), and hands the bytes between.

## The `<terminal>` element

The declarative shape is one element. Spawn a pty from your model (`fx.ptySpawn`, an ordinary effect), then bind its key to a `<terminal>`: the toolkit owns the emulator behind that key and the element renders its grid — real text, geometric box drawing, the theme-derived ANSI palette, selection, cursor, and scrollback — and routes keys, IME text, and wheel scrollback to the session when focused. The model carries a pty key and, if it wants, the scrollback position; nothing about the emulator's cells, modes, or selection.

```ts
import { Cmd } from "@native-sdk/core";

const shellKey = 1;

export function view(model: Model) {
  return terminal({ pty: shellKey, scrollback: model.scrollback, grow: 1, label: "Shell", onTerminal: "termState" });
}

export function update(model: Model, msg: Msg): Model {
  switch (msg.kind) {
    // The runtime-applied view state (scrollback, history, cols, rows).
    // Echo scrollback back and user scrollback survives rebuilds.
    case "termState": return { ...model, scrollback: msg.state.scrollback };
  }
}
```

```zig
const shell_key: u64 = 1;

pub fn view(ui: *Ui, model: *const Model) Ui.Node {
    return ui.terminal(.{
        .pty = shell_key,
        .scrollback = model.scrollback,
        .grow = 1,
        .semantics = .{ .label = "Shell" },
        .on_terminal = Ui.terminalMsg(.term_state),
    });
}

pub fn update(model: *Model, msg: Msg) void {
    switch (msg) {
        // canvas.TerminalState: scrollback, history, cols, rows — the
        // app-visible view state. Echo scrollback back under the same
        // source-wins rule scroll offsets follow; the emulator's cell
        // state stays framework-owned.
        .term_state => |state| model.scrollback = state.scrollback,
    }
}
```

`pty` is the model-owned pty key — one `{binding}`, never a markup literal, exactly like `<media-surface surface>` and `<image image>`. A `<terminal>` without it is refused at validation the way a `<media-surface>` without `surface` is: dead markup can never attach a session. The grid derives its `cols`/`rows` from the frame the layout gives it, so size the element like any leaf (`grow`, or a definite `width`/`height`); the runtime pushes the derived size to the pty with `ptyResize`.

Behind the binding, the runtime owns the session: it feeds the key's output batches into the emulator as they are journaled, encodes the focused element's keys and committed IME text back through `ptyWrite`, answers the device queries a program sends (cursor position, device attributes), scrolls history on the wheel, and reports the applied view state through `on-terminal`. Pointer selection follows terminal conventions: drag selects cells, double-click selects a word, triple-click selects a line, and Cmd/Ctrl+C copies the emulator's selected text instead of forwarding the chord to the child. Because every byte in both directions crosses the journaled effect boundary, a recorded session replays to the same screen with no shell present — the same guarantee the raw vocabulary gives, through the element.

### Enabling live sessions

The emulator is opt-in per app, so an app that never renders a terminal never pays for one. Pin `ghostty` as a **lazy** dependency in your app's `build.zig.zon` and ask for sessions in its `build.zig`:

```zig
pub fn build(b: *std.Build) void {
    _ = native_sdk.addAppArtifacts(b, b.dependency("native_sdk", .{}), .{
        .name = "workbench",
        .terminal_sessions = true,
    });
}
```

Without that flag — every scaffolded app, by default — `<terminal>` renders its empty surface and the build never traverses the emulator's dependency graph. `native_sdk.runtime.terminal_sessions_enabled` reports which half the build carries, so an app's tests can gate on it.

[`examples/workbench`](https://github.com/vercel-labs/native/tree/main/examples/workbench) is the element end to end: a live shell beside a browser in one resizable split, with no emulator wiring in the app at all.

The rest of this page is the effect vocabulary the element sits on — spawn a pty yourself when you want the raw stream, or let the element own the session end to end.

## Spawn, then stream

The pty key is ordinary model data — pick it like an effect key. Spawn the shell, feed each output batch into the emulator, and write typed input back to the pty. Exactly one exit event ends the session.

```ts
import { Cmd, asciiBytes } from "@native-sdk/core";

const shellKey = 1;

export function update(model: Model, msg: Msg): Model | [Model, Cmd<Msg>] {
  switch (msg.kind) {
    case "start":
      return [model, Cmd.ptySpawn(
        [asciiBytes("/bin/zsh"), asciiBytes("-i")],
        { cols: model.cols, rows: model.rows, event: "shell" },
      )];
    case "shell":
      // A coalesced output batch, or the exactly-one exit.
      if (msg.kind2 === "output") return { ...model, screen: feed(model.screen, msg.bytes) };
      return { ...model, phase: msg.reason === "exited" ? "ended" : "failed" };
    case "type":
      return [model, Cmd.ptyWrite(shellKey, msg.bytes)];
    case "viewport":
      return [model, Cmd.ptyResize(shellKey, msg.cols, msg.rows)];
  }
}
```

```zig
const shell_key: u64 = 1;

fn initFx(model: *Model, fx: *Fx) void {
    fx.ptySpawn(.{
        .key = shell_key,
        .argv = &.{ "/bin/zsh", "-i" },
        .cols = model.cols,
        .rows = model.rows,
        .on_event = Fx.ptyMsg(.shell),
    });
}

pub fn update(model: *Model, msg: Msg, fx: *Fx) void {
    switch (msg) {
        .shell => |event| switch (event.kind) {
            // A coalesced output batch: feed the emulator, then flush
            // any query answers (DSR, DA1) back to the pty.
            .output => {
                model.session.feed(event.bytes);
                const answers = model.session.pendingResponses();
                if (answers.len > 0) _ = fx.ptyWrite(shell_key, answers);
                model.session.clearResponses();
            },
            // The exactly-one terminal: exited, signaled, cancelled,
            // rejected, or spawn_failed.
            .exit => model.phase = if (event.reason == .exited) .ended else .failed,
            // Write-admission verdicts are journal/replay machinery;
            // they never arrive as events.
            .write => unreachable,
        },
        // Typed text goes straight to the child's stdin. `ptyWrite`
        // returns whether the payload was accepted whole — a caller
        // that must not lose bytes retains a refused payload and
        // retries as the child reads (see the example's outbound ring).
        .text => |event| _ = fx.ptyWrite(shell_key, event.text),
        .viewport => |size| {
            model.session.resize(size.cols, size.rows);
            fx.ptyResize(shell_key, size.cols, size.rows);
        },
    }
}
```

The full app lives in [`examples/terminal`](https://github.com/vercel-labs/native/tree/main/examples/terminal): the grid renderer, keyboard-first selection and scrollback, clipboard copy, and the record/replay tests.

## The vocabulary

<table>
  <thead>
    <tr>
      <th>
        Verb
      </th>

      <th>
        What it does
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `ptySpawn`
      </td>

      <td>
        Open a pty, fork

        `argv`

        onto it as its controlling terminal, and stream output back through

        `on_event`

        . Initial

        `cols`

        /

        `rows`

        and an optional

        `term`

        (default

        `xterm-256color`

        ).
      </td>
    </tr>

    <tr>
      <td>
        `ptyWrite`
      </td>

      <td>
        Bytes toward the child's stdin (keystrokes, pastes, query answers), all-or-nothing: returns whether the payload was accepted, so a caller that must not lose bytes retains a refusal and retries. Journaled as command payloads — verdicts included — so replay reproduces the recorded results.
      </td>
    </tr>

    <tr>
      <td>
        `ptyResize`
      </td>

      <td>
        Push a new grid size; the child receives SIGWINCH and re-queries (on Windows,

        `ResizePseudoConsole`

        and the console window-size change).
      </td>
    </tr>

    <tr>
      <td>
        `ptyKill`
      </td>

      <td>
        Terminate the child's job (SIGKILL to its process group — the whole foreground job; a descendant that re-grouped itself escapes, POSIX offering no kill-whole-session primitive. On Windows:

        `TerminateProcess`

        plus the pseudoconsole teardown, which reaches everything still attached to the console). The exit then reports

        `cancelled`

        .
      </td>
    </tr>
  </tbody>
</table>

A pty is **a spawn with a different transport**, so it rides the same seams as `Cmd.spawn`: the same `command` permission in [`app.zon`](/docs/app-zon), the same environment policy (the child inherits the host environment the app bound, plus `TERM`), the same argv budgets, and the same one key space as spawns, fetches, and channels — a pty key is occupied from spawn until its exit Msg delivers.

## Output is coalesced, never per-read

Output arriving between frames coalesces into **one batch per drain**, bounded at 64 KiB. `cat largefile` does not journal a record per `read()` — it journals per-frame batches, so a firehose stays a handful of Msgs per frame and the session journal stays small. The transport applies real back-pressure to the child (its staging ring is lossless: a full ring parks the reader and the kernel slows the writer, exactly a terminal's native flow control), so **no byte is ever dropped** — a fast producer is paced, not truncated.

<span id="one-exit-honest-classes" />

## Exit outcomes

Exactly one `.exit` event ends every spawn — accepted or refused:

- **`exited`** carries the child's exit code.
- **`signaled`** carries the terminating signal number (the code is `-1`). POSIX only — Windows has no signal channel; see the platform notes below.
- **`cancelled`** follows `ptyKill` — after the verb, the exit always says so, even if the child was already exiting.
- **`rejected`** is a request refused before a child existed: an over-budget argv, a zero grid dimension, a duplicate live key, a full pty table, or a platform without pty support.
- **`spawn_failed`** is a pty or exec that could not start (a missing binary).

The exit also reports `dropped_writes` — `ptyWrite` payloads refused over the session's life (an over-bound single write, or a child that stopped reading and filled the outbound buffer). Zero means every keystroke reached the child.

One contract rides the whole family: the toolkit forks each pty child, is its **sole reaper**, and never touches SIGCHLD disposition — an embedding process must not either. A host that ignores SIGCHLD (the kernel auto-reaps), sets `SA_NOCLDWAIT`, or `wait()`s on children it did not spawn frees pids outside the toolkit's kill/reap fence; POSIX offers no portable way to signal a pid safely once a third party can reap it first.

## Rendering the grid

The emulator owns cell state and damage; the app paints the viewport as real text through the canvas primitives. The example maps the palette as follows:

- **ANSI-16** derives from the active [theme tokens](/docs/theming) wherever the emulator still holds its default palette entry — so a themed app and its terminal read as one surface. The moment a program restyles a slot (OSC 4), the programmed color wins verbatim.
- **256-color** (the cube and the grayscale ramp) and **truecolor** always pass through exactly.
- **Wide CJK cells** occupy two columns with a spacer tail, straight from the emulator's width semantics; the [registered mono font](/docs/fonts) supplies the glyphs.

Scrollback windows the viewport through the emulator's page list; keyboard-first selection (line and block) reads real cell text for clipboard copy. Pointer-drag selection is not wired in the example — the grid is keyboard-first by design.

## Recorded sessions replay byte-identical, offline

The output bytes **are** the effect result, and the session journal treats them the way [dynamic images](/docs/dynamic-images) treat decoded pixels. When a recorded session streams pty output, each batch's bytes are written — at effect-result time — into the content-addressed blob store beside the journal (`blobs/<sha256[..16]>`), and the journal record carries the hash and length. Identical batches (a repeated prompt, a redrawn screen) share one blob.

**Replay never spawns a process.** The `ptySpawn` is an ordinary replayed dispatch that parks the pty (the key registers as live; `ptyWrite`/`ptyResize`/`ptyKill` send nothing — the recorded output already carries their consequences), and the journaled output batches and exit feed verbatim: the blob store supplies the bytes, the emulator re-runs the same VT parsing, and the fingerprint checkpoints verify the replayed grid against the recording frame by frame. `ptyWrite` admission verdicts are journaled too — whether the outbound buffer had room is executor truth, like the bytes themselves — so a replayed write returns exactly what the live one did and an app that retains refused input takes the identical path. A journal whose `blobs/` directory is missing or damaged refuses loudly rather than replaying a different session.

```sh
NATIVE_SDK_SESSION_RECORD=session/app.journal native run   # records blobs/ beside the journal
NATIVE_SDK_SESSION_REPLAY=session/app.journal native run   # replays offline, verifying fingerprints
```

Adding the pty record kind moved the journal format's layout fingerprint: recordings from older builds are refused at the preamble with a diagnostic requesting a new recording.

## Platform support

<table>
  <thead>
    <tr>
      <th>
        Platform
      </th>

      <th>
        Pty transport
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        macOS
      </td>

      <td>
        Supported —

        `openpty`

        \+ a controlling terminal via libSystem.
      </td>
    </tr>

    <tr>
      <td>
        Linux
      </td>

      <td>
        Supported —

        `openpty`

        \+ a controlling terminal (glibc 2.34+ or musl).
      </td>
    </tr>

    <tr>
      <td>
        Windows
      </td>

      <td>
        Supported on Windows 10 1809+ (the first ConPTY release; the backend links

        `CreatePseudoConsole`

        directly, so that is the binary's floor) — ConPTY over an overlapped pipe pair; UTF-8 with VT sequences in both directions, which is the pseudoconsole's own pipe contract. Behavioral differences below.
      </td>
    </tr>

    <tr>
      <td>
        Null / headless
      </td>

      <td>
        A scriptable fake pty drives the whole vocabulary in tests — spawn requests, output and exit feeds, write capture — with no process, so CI is fully deterministic.
      </td>
    </tr>
  </tbody>
</table>

### Windows semantics, stated plainly

The vocabulary is identical — the terminal example runs unchanged — but ConPTY differs from a POSIX pty in ways an app may observe:

- **Exit codes only.** Windows has no signals, so `signaled` never occurs there: every ending is `exited` with a process exit code (a crash surfaces the NTSTATUS bit-cast to `i32` — an access violation's `0xC0000005` arrives negative), `cancelled` after `ptyKill`, or the shared refusal classes. Apps that branch on `signal` should treat it as the POSIX-only detail it is.
- **Rendered stream, not raw bytes.** ConPTY output is conhost's VT rendering of the child's screen, not the child's literal write stream — a terminal emulator consumes it identically, but byte-exact expectations against child output do not transfer.
- **`ptyKill` reach.** The kill is `TerminateProcess` on the direct child plus the pseudoconsole teardown, which takes down every descendant still attached to the console; a descendant that detached (`FreeConsole`, `DETACHED_PROCESS`) escapes — the same documented limit as a POSIX descendant that left the process group.
- **`ptyResize`** maps to `ResizePseudoConsole`; conhost delivers the window-size change to the child through the console APIs (the SIGWINCH analog). Console geometry is 16-bit signed there, so dimensions above 32767 clamp to it — the declared mirrors keep the requested value, the child observes the clamp.
- **Session lifetime follows the direct child.** Conhost never breaks the output pipe on its own — not even after every attached process has exited — so the backend watches the child's process handle and closes the pseudoconsole once the pipe has stayed quiet past a bounded grace after its exit. An attached descendant that outlives the direct child (a launcher's `start /b` grandchild) ends with the console teardown, where a POSIX descendant holding the pty child end would have kept the session open; a detached one escapes, as above.
- **Environment policy is unchanged** — the child sees the bound host environment plus `TERM` — with one Windows spelling: environment names are case-insensitive there, so a caller-supplied `Term` is still the entry the injected value replaces. `argv[0]` resolves through the caller environment's `PATH` (`;`-separated) against the executable kinds the spawn can start (`.exe`, `.cmd`, `.bat`, `.com`, tried before a bare extension-less spelling); the current directory is deliberately never searched.
- **Batch targets run through an explicit `cmd.exe /d /s /c`** (AutoRun disabled, standard quote treatment), and arguments — or a script path — carrying bytes cmd's reparse could reinterpret (`& | < > ^ % ! "` or line breaks) refuse the spawn: cmd's grammar has no lossless quoting for them (`%` expands inside and outside quotes), so delivering them verbatim to a script is not something any spawner can promise — refusal over a silently different command line. Direct executables carry the full argv byte-exactly, as everywhere.
- **The reaping contract reads differently.** Windows process objects self-release when the last handle closes, so the POSIX sole-reaper/SIGCHLD paragraph above has no Windows analog — there is nothing an embedder can reap out from under the toolkit.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Theming
url: "https://native-sdk.dev/docs/theming"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Built-in components use `DesignTokens` for their default colors, typography, dimensions, and interaction styles. Select a built-in theme pack, apply sparse token overrides, or provide a complete token set. Zig views can also set local `ElementOptions.style` overrides; these take precedence over token references.

## Theme packs

A theme pack supplies a complete set of design tokens. Two packs are available:

- `house` (the default) — the monochrome neutral token set documented throughout these pages.
- `geist` — the design token set of the bundled Geist typeface family: a cool neutral scale over pure white/black pages, monochrome primaries, blue focus rings, 6px control corners, and a taller 32/40/48 control ladder.

Select a pack in `app.json`:

```json
{
  "id": "com.example.app",
  "name": "example",
  "version": "0.1.0",
  "theme": "geist"
}
```

An unknown name is a build error listing the valid packs — never a silent fallback. The scaffold passes the manifest's choice into the app through `app_runner.manifestThemePack()`; wire it to your `UiApp` options and the pack composes with the live system appearance, so a packed app still re-themes on the OS light/dark flip (plus high contrast and reduced motion), no restart:

```zig
const MyApp = native_sdk.UiApp(Model, Msg);
// In your options:
.theme = app_runner.manifestThemePack(),
```

In a zero-config TypeScript app, export `themeState(model)` when pack, color scheme, or accent belongs in live app state. The generated launcher recognizes the helper and wires the stock-token path automatically:

```ts
import { type ThemeState } from "@native-sdk/core/events";

export type ThemePack = "house" | "geist";
export type ThemePreference = "system" | "light" | "dark";

export interface Model {
  readonly theme: ThemePack;
  readonly themePreference: ThemePreference;
  readonly pinkAccent: boolean;
}

export function themeState(model: Model): ThemeState {
  if (model.pinkAccent) {
    return {
      pack: model.theme,
      colorScheme: model.themePreference,
      accent: "#df2670",
    };
  }
  return { pack: model.theme, colorScheme: model.themePreference };
}
```

`ThemeState` has five optional fields:

```ts
export type ThemeState = {
  readonly pack?: "house" | "geist";
  readonly colorScheme?: "light" | "dark" | "system";
  readonly accent?: string; // exactly #rrggbb
  readonly highContrast?: boolean;
  readonly reduceMotion?: boolean;
};
```

Change the model through ordinary messages. The helper runs after every committed update. Omitted fields inherit the next lower layer; omitted `colorScheme` and `"system"` both follow the OS. An invalid accent produces a runtime error. Omitted `highContrast` and `reduceMotion` follow the OS. Explicit values, including `false`, retain model-owned appearance during replay. Effective high contrast suppresses manifest and model accent overrides.

The stock-theme precedence is:

<table>
  <thead>
    <tr>
      <th>
        Axis
      </th>

      <th>
        Highest to lowest precedence
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        Complete tokens
      </td>

      <td>
        `tokens_fn`

        → static

        `tokens`

        → stock theme composition below
      </td>
    </tr>

    <tr>
      <td>
        Pack
      </td>

      <td>
        `themeState.pack`

        →

        `manifest theme`

        →

        `house`
      </td>
    </tr>

    <tr>
      <td>
        Color scheme
      </td>

      <td>
        forced

        `themeState.colorScheme`

        → OS appearance
      </td>
    </tr>

    <tr>
      <td>
        Accent
      </td>

      <td>
        `themeState.accent`

        →

        `manifest theme_accent`

        → selected pack
      </td>
    </tr>

    <tr>
      <td>
        High contrast / reduced motion
      </td>

      <td>
        explicit

        `themeState.highContrast`

        /

        `themeState.reduceMotion`

        → OS appearance (effective high contrast suppresses accent overrides)
      </td>
    </tr>

    <tr>
      <td>
        Surface scale / text measurement
      </td>

      <td>
        Runtime-stamped last
      </td>
    </tr>
  </tbody>
</table>

`themeState` deliberately controls canvas design tokens in v1. Native title bars and WebViews still follow the platform's effective appearance; forcing dark canvas content does not call `NSApp.appearance` or impose a scheme on embedded web content.

The earlier `themePack(model): "house" | "geist"` helper remains supported unchanged for apps that only switch packs. It preserves live OS scheme, manifest accent, high contrast, reduced motion, and surface scale. Export `themePack` or `themeState`, never both; the checker and adapter report that conflict as an error.

Apps that derive their own tokens select the pack directly — `ThemeOptions.pack` is just another theme axis, exactly as switchable at runtime as the scheme:

```zig
pub fn myTokens(model: *const Model) canvas.DesignTokens {
    return canvas.DesignTokens.theme(.{
        .color_scheme = model.system_scheme,
        .pack = if (model.brand_preview) .geist else .house,
    });
}
```

Pack selection is deliberately manifest/API vocabulary, not markup: a document describes structure, the app owns its look.

## Pin your design system in CI

To detect rendering changes, render a reference screen in a test and pin its pixel signature. Use the same renderer, fonts, scale, and environment for each run. The SDK uses reference signatures for both built-in packs. Review light and dark captures before updating a signature; the test covers the rendered screen, rather than every possible component state.

## The overrides layer

`DesignTokenOverrides` applies a sparse set of optional fields over a base theme. Null fields keep the base value:

```zig
var tokens = canvas.DesignTokens.themeWithOverrides(.{
    .color_scheme = scheme,
    .pack = .geist,
}, .{
    // Brand accent over the pack's monochrome primary.
    .colors = .{ .accent = canvas.Color.rgb8(0, 120, 111), .accent_text = canvas.Color.rgb8(240, 253, 250) },
    // Sharper corners across the token set.
    .radius = .{ .sm = 4, .md = 4, .lg = 4 },
});
```

The layering order is always: pack token set → your overrides → runtime-stamped values (the surface scale in `pixel_snap.scale`, platform text measurement). Overrides exist for every token group, including the control tables (`controls.button_primary.background = …`), the interaction-state formulas (`states`), and the metric ladder (`metrics`).

## The token groups

The token groups are:

- `colors` — the semantic palette: page/surface/washes, text inks, border hairline, the accent pair, the four semantic hues (destructive/success/warning/info) with their knockout inks, focus ring, shadow ink, scrim, disabled fill.
- `controls` — per-control visual tables (`button_primary`, `input`, `card`, …). Each entry states background/hover/active/pressed/disabled colors, foreground, border, radius, and stroke width; null fields fall through to the semantic palette and the state formulas. This is where a design system's per-control exceptions live — a filled error button, a 12px card corner — without touching any renderer.
- `states` — the interaction formulas applied when a control table is silent: the filled hover/pressed alpha cuts, the disabled wash strength, the destructive chip's wash ladder, the static-text selection wash.
- `metrics` — the control size token set: the sm/default/lg height ladder, per-rung button insets, button label steps, icon extents and gaps, row extent.
- `typography` — faces and the type rungs (body/label/title/button/heading/display). The face slots (`font_id`/`mono_font_id`) take app-registered faces too — see [Fonts](/docs/fonts).
- `radius`, `stroke`, `spacing` — the corner scale, hairline/focus stroke widths plus the focus ring's offset gap, and the spacing scale.
- `shadow`, `blur`, `motion`, `scroll`, `layer`, `pixel_snap` — elevation, backdrop blur, motion durations/easing, scroll physics, z-bands, and snapping.

## Authoring a full design system

To provide a complete design system, return a `DesignTokens` value for each supported color scheme and contrast setting:

1. Use your system's published values: the neutral scale, the accent, the semantic hues, the type rungs, the radii, the control heights.
2. Fill `colors` for all four scheme/contrast pairs (light, dark, and both high-contrast variants — high contrast is not optional; push inks to their extremes and borders toward opaque).
3. Use `controls` for per-control exceptions. Prefer the shared `colors` palette for values used across controls.
4. Restate `states` and `metrics` only if your system's interaction feedback or size ladder differs from the defaults.
5. Pin a reference signature (see above) and review light and dark captures by eye before blessing it.

```zig
pub fn brandTokens(scheme: canvas.ColorScheme, contrast: canvas.ColorContrast) canvas.DesignTokens {
    return .{
        .colors = brandColors(scheme, contrast),
        .controls = brandControls(scheme, contrast),
        .typography = .{ .body_size = 14, .title_size = 20 },
        .radius = .{ .sm = 4, .md = 6, .lg = 8, .xl = 12 },
        .metrics = .{ .control_height = 36 },
    };
}
```

Hand it to your app via `tokens_fn` (model-owned, follows the system scheme through your model) or `tokens` (fixed). These complete-token paths take precedence over `themeState`; the runtime stamps `pixel_snap.scale` and text measurement after your function runs, so never cache those.

## What themes cannot do

Tokens change visual defaults. They do not change control behavior, states, semantics, or layout structure. Use component composition when you need a different structure.

## Theme, eject, or build

Choose the customization path according to the change you need:

- **Theme it.** Engine controls — buttons, text fields, tabs, every widget whose behavior lives in the runtime — are themed, never forked. The depth above ([packs](#theme-packs), [overrides](#the-overrides-layer), [full design systems](#authoring-a-full-design-system)) sets their visual defaults, with per-control overrides through the `controls` tables.
- **Eject it.** Library composites — views the SDK itself builds out of primitives — can become your code when you need to own their shape: `native eject component <name>` writes the canonical source into `src/components/`, building the identical widget tree at the moment of ejection. See [Building Components](/docs/building-components#use-eject-or-build) for the ejectable set and the mechanics.
- **Build it.** When neither fits, compose a new component from the same primitives the library uses — [Building Components](/docs/building-components) walks through it. Use the shared tokens in your component so it follows the app theme.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: System Tray
url: "https://native-sdk.dev/docs/tray"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The Native SDK supports system tray icons with menus. Tray menu items can dispatch explicit command names with `source = .tray`; items without a command dispatch the compatibility command name `"tray.action"`.

Tray support is currently implemented on macOS and Windows system WebView hosts. Linux returns `UnsupportedService` until a portable status notifier implementation is selected.

On macOS, `title` renders the tray as a menu-bar extra: a titled `NSStatusItem` with variable width. When both `icon_path` and `title` are empty, the button falls back to the app name's first letter.

## TrayOptions

<table>
  <thead>
    <tr>
      <th>
        Field
      </th>

      <th>
        Type
      </th>

      <th>
        Default
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `icon_path`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `""`
      </td>
    </tr>

    <tr>
      <td>
        `title`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `""`
      </td>
    </tr>

    <tr>
      <td>
        `tooltip`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `""`
      </td>
    </tr>

    <tr>
      <td>
        `items`
      </td>

      <td>
        `[]const TrayMenuItem`
      </td>

      <td>
        <code>&.</code>
      </td>
    </tr>

    <tr>
      <td>
        `presentation`
      </td>

      <td>
        `TrayPresentation`
      </td>

      <td>
        <code>.</code>
      </td>
    </tr>

    <tr>
      <td>
        `activation_command`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `""`
      </td>
    </tr>

    <tr>
      <td>
        `alternate_activation_command`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `""`
      </td>
    </tr>

    <tr>
      <td>
        `open_command`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `""`
      </td>
    </tr>
  </tbody>
</table>

## TrayMenuItem

<table>
  <thead>
    <tr>
      <th>
        Field
      </th>

      <th>
        Type
      </th>

      <th>
        Default
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `id`
      </td>

      <td>
        `TrayItemId`

        (

        `u32`

        )
      </td>

      <td>
        `0`
      </td>
    </tr>

    <tr>
      <td>
        `label`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `""`
      </td>
    </tr>

    <tr>
      <td>
        `command`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `""`
      </td>
    </tr>

    <tr>
      <td>
        `separator`
      </td>

      <td>
        `bool`
      </td>

      <td>
        `false`
      </td>
    </tr>

    <tr>
      <td>
        `enabled`
      </td>

      <td>
        `bool`
      </td>

      <td>
        `true`
      </td>
    </tr>

    <tr>
      <td>
        `detail`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `""`
      </td>
    </tr>

    <tr>
      <td>
        `role`
      </td>

      <td>
        `TrayItemRole`
      </td>

      <td>
        `.command`
      </td>
    </tr>

    <tr>
      <td>
        `key`
      </td>

      <td>
        `[]const u8`
      </td>

      <td>
        `""`
      </td>
    </tr>

    <tr>
      <td>
        `modifiers`
      </td>

      <td>
        `ShortcutModifiers`
      </td>

      <td>
        <code>.</code>
      </td>
    </tr>

    <tr>
      <td>
        `segmented`
      </td>

      <td>
        `?TraySegmentedRow`
      </td>

      <td>
        `null`
      </td>
    </tr>

    <tr>
      <td>
        `metric`
      </td>

      <td>
        `?TrayMetricRow`
      </td>

      <td>
        `null`
      </td>
    </tr>

    <tr>
      <td>
        `chart`
      </td>

      <td>
        `?TrayChartRow`
      </td>

      <td>
        `null`
      </td>
    </tr>
  </tbody>
</table>

## PlatformServices methods

Use the runtime methods from app code:

- `runtime.createStatusItem(id, options)` -- create a status item under a stable non-zero id
- `runtime.updateStatusItemShell(id, shell)` -- update icon, tooltip, visibility, and activation/open commands in place
- `runtime.updateStatusItemMenu(id, items)` -- update one menu without recreating its status item
- `runtime.updateStatusItemPresentation(id, presentation)` -- update one live title and visual presentation
- `runtime.removeStatusItem(id)` -- remove only the identified item
- `runtime.createTray(options)` -- create or replace the tray icon
- `runtime.updateTrayMenu(items)` -- update menu items without recreating the tray
- `runtime.updateTrayTitle(title)` -- update only the live tray title
- `runtime.updateTrayPresentation(presentation)` -- update the live title and visual presentation
- `runtime.removeTray()` -- remove the tray icon

The singular `*Tray` methods are compatibility wrappers for reserved status-item id `1`. The lower-level `PlatformServices` surface exposes the same keyed methods for platform adapters. `Runtime` supports at most eight simultaneous status items and validates every menu independently: non-separator rows need a label, command-backed rows need a unique non-zero row `id`, and each menu is capped at 32 rows.

## TypeScript: model-derived status items

In a TypeScript app, export `statusItem(model)` from `src/core.ts`. The generated launcher installs it from the committed boot model and re-runs it after every model update. Shell, presentation, and menu are hashed independently, so changing the icon, tooltip, click hooks, title, width, tone, icon opacity, typography, or rows patches only that channel and never recreates the native status item.

```ts title="src/core.ts"
import { asciiBytes, utf8Bytes } from "@native-sdk/core";
import { type StatusItemState } from "@native-sdk/core/events";

export function statusItem(model: Model): StatusItemState {
  return {
    iconPath: asciiBytes("assets/menu-bar.svg"),
    tooltip: utf8Bytes("Player status"),
    activationCommand: asciiBytes("app.refresh"),
    alternateActivationCommand: asciiBytes("player.toggle"),
    openCommand: asciiBytes("app.refresh"),
    presentation: {
      title: model.playing ? utf8Bytes("MB PLAY") : utf8Bytes("MB"),
      width: model.playing ? 72 : 48,
      tone: model.failed ? "critical" : "normal",
      iconOpacity: model.stale ? 0.5 : 1,
      monospaced: true,
      fontSize: 13,
      fontWeight: "semibold",
    },
    items: [
      { id: 10, label: model.today, command: asciiBytes(""), separator: false, enabled: false, detail: model.quota, role: "hero", key: asciiBytes(""), modifiers: { primary: false, command: false, control: false, option: false, shift: false } },
      { id: 0, label: asciiBytes(""), command: asciiBytes(""), separator: true, enabled: false, detail: asciiBytes(""), role: "command", key: asciiBytes(""), modifiers: { primary: false, command: false, control: false, option: false, shift: false } },
      { id: 3, label: utf8Bytes("Settings…"), command: asciiBytes("app.settings"), separator: false, enabled: true, detail: asciiBytes(""), role: "command", key: asciiBytes(","), modifiers: { primary: false, command: true, control: false, option: false, shift: false } },
    ],
  };
}
```

`iconPath`, `tooltip`, `activationCommand`, `alternateActivationCommand`, and `openCommand` update live alongside presentation and rows. A normal click emits `activationCommand` and opens the menu; an Option-click emits `alternateActivationCommand` without opening it. Every menu open emits `openCommand`, which is useful for an on-demand refresh while the background cadence stays slow. These lifecycle hooks dispatch with `source = .tray`; empty commands disable them.

For multiple independent items, export `statusItems(model): readonly StatusItemDescriptor[]` instead. Each descriptor has the same shell, presentation, and row fields plus a stable non-zero `id` and live `visible` flag. Presence creates, absence removes, and changed fields patch only that identifier; menus update without replacing their `NSStatusItem`. Export either `statusItem` or `statusItems`, not both. For example, a metric item can appear or disappear while a separate menu item remains visible.

Persistent menu-bar composition uses that collection directly: return one descriptor for compact context text, another for a larger live metric, and another icon-only descriptor that owns the dropdown. Each descriptor independently chooses optional `fontSize` (omitted or `0` keeps the platform default), optional `fontWeight` (omitted means `regular`; otherwise `regular`, `medium`, `semibold`, or `bold`), `monospaced`, width, tone, and icon opacity. They remain separate native status items, so each can appear, disappear, or update without recreating its neighbors.

Rows use the exact `StatusItemMenuItem` record. `role` is `command`, `info`, `header`, `hero`, `agent`, `context`, `segmented`, or `chart`; capable macOS hosts render the readout roles as native rich content while simpler hosts degrade them to text. `detail` carries secondary readout content, and `key` plus the five explicit `modifiers` fields declares a menu equivalent. Actionable rows need unique non-zero ids; separators conventionally use id 0 and empty byte fields. The menu may contain at most 32 rows. Map every row/click/open command to an ordinary message with `commandMsg(name): Msg | null`; no Zig `status_item_fn` glue is needed.

### Typed rich rows

A segmented row carries its choices as data rather than encoding them into `label` or `detail`. Each option has its own stable `id`, label, command, selected state, and enabled state. macOS renders the row as `NSSegmentedControl`; selecting a segment emits its option id through the same tray-action → command → `commandMsg` route as an ordinary command row. Other hosts may expose the options as separate command items.

```ts
{
  id: 0,
  label: asciiBytes(""),
  command: asciiBytes(""),
  separator: false,
  enabled: true,
  detail: asciiBytes(""),
  role: "segmented",
  key: asciiBytes(""),
  modifiers: { primary: false, command: false, control: false, option: false, shift: false },
  segmented: {
    options: [
      { id: 20, label: asciiBytes("Day"), command: asciiBytes("range.day"), selected: model.range === "day", enabled: true },
      { id: 21, label: asciiBytes("Week"), command: asciiBytes("range.week"), selected: model.range === "week", enabled: true },
    ],
  },
}
```

A typed metric row keeps prominent primary and secondary text inside the dropdown, separate from the persistent menu-bar title:

```ts
{
  id: 0,
  label: asciiBytes(""),
  command: asciiBytes(""),
  separator: false,
  enabled: false,
  detail: asciiBytes(""),
  role: "hero",
  key: asciiBytes(""),
  modifiers: { primary: false, command: false, control: false, option: false, shift: false },
  metric: {
    primaryText: asciiBytes("2,494 requests"),
    secondaryText: utf8Bytes("Today · production"),
    accessibilityLabel: asciiBytes("2,494 requests today in production"),
  },
}
```

A chart row carries 1–32 finite values plus an explicit numeric domain, leading caption, trailing summary, and required accessibility label. Every value must fall inside `minValue...maxValue`. The macOS system host draws native AppKit bars in a custom `NSView`; hosts without custom tray rows retain the caption and summary as text.

```ts
{
  id: 0,
  label: asciiBytes(""),
  command: asciiBytes(""),
  separator: false,
  enabled: false,
  detail: asciiBytes(""),
  role: "chart",
  key: asciiBytes(""),
  modifiers: { primary: false, command: false, control: false, option: false, shift: false },
  chart: {
    values: model.cpuHistory,
    minValue: 0,
    maxValue: 1,
    leadingCaption: asciiBytes("CPU"),
    trailingSummary: model.cpuSummary,
    accessibilityLabel: model.cpuAccessibility,
  },
}
```

A segmented row admits at most eight options and at most one selected option. Option ids share the enclosing menu’s command-id namespace, including other segmented rows. The 32-row menu budget is also the fallback budget: a segmented row consumes one fallback row per option, preventing silent truncation on plain-menu hosts.

Use `utf8Bytes` for titles, labels, tooltips, and details; it preserves characters such as `…`, `·`, and emoji as UTF-8. Use `asciiBytes` for guaranteed-ASCII command names, keys, paths, and empty byte fields. Passing non-ASCII literal/template text to `asciiBytes` is an NS1064 build error.

## Handling tray actions

When a user clicks a tray menu item, the runtime dispatches a `CommandEvent` with source `.tray`, the native `status_item_id`, and that menu's `tray_item_id`. Row ids only need to be unique within their own menu. Prefer command-backed items when the item represents a known app action:

```zig
try runtime.createTray(.{
    .tooltip = "native-sdk",
    .items = &.{
        .{ .id = 1, .label = "Refresh", .command = "app.refresh" },
        .{ .separator = true },
        .{ .id = 2, .label = "Quit", .command = "app.quit" },
    },
});
```

Use your `event_fn` to handle the commands — every declared name gets a real consequence, `"app.quit"` included:

```zig
fn event(context: *anyopaque, runtime: *Runtime, ev: Event) anyerror!void {
    switch (ev) {
        .command => |cmd| {
            if (std.mem.eql(u8, cmd.name, "app.refresh")) {
                // Refresh the model, re-render.
            } else if (std.mem.eql(u8, cmd.name, "app.quit")) {
                // The REAL graceful terminate: the host emits the same
                // shutdown event a last-window close does, so the stop
                // hook runs exactly once.
                try runtime.quitApp();
            }
        },
        else => {},
    }
}
```

## Menu-bar extras in `UiApp`

Canvas-first apps declare the status item once, with `UiApp.Options.status_item` (installed on the installing frame). Selecting a menu item dispatches its `command` through the ordinary `on_command` mapping with source `.tray`:

```zig
app.* = PreviewApp.init(allocator, .{}, .{
    // ...
    .on_command = command,
    .status_item = .{
        .title = "ZN",
        .tooltip = "Native SDK Canvas Preview",
        .items = &.{
            .{ .id = 1, .label = "Show Docs", .command = "app.docs" },
            .{ .separator = true },
            .{ .id = 2, .label = "Reload Preview", .command = "app.reload" },
        },
    },
});
```

macOS implements this through `NSStatusItem`; platforms without a status-bar service log a warning and continue. See `examples/canvas-preview` for the live composition.

## Model-driven title and menu in Zig-core apps

For a live menu-bar extra — an open-count badge in the title, the latest items in the dropdown — add `UiApp.Options.status_item_fn`. It is consulted on install and after every rebuild, and the runtime re-applies only what actually changed: shell, presentation, and menu changes patch independently without flicker or native-item recreation. The static `status_item` provides defaults for icon, tooltip, activation, alternate-activation, and open commands; the callback may update those fields live too.

```zig
fn statusItem(model: *const Model, scratch: *App.StatusItemScratch) App.StatusItemState {
    const title = std.fmt.bufPrint(&scratch.title_buffer, "ZN {d}", .{model.open_count}) catch "ZN";
    scratch.items[0] = .{ .id = 1, .label = "Refresh", .command = "app.refresh" };
    scratch.items[1] = .{ .separator = true };
    scratch.items[2] = .{ .id = 10, .label = model.latest_title, .command = "issue.select.latest" };
    return .{
        .presentation = .{ .title = title, .width = 62, .monospaced = true },
        .items = scratch.items[0..3],
    };
}
// options: .status_item_fn = statusItem,
```

Selections dispatch each item's `command` through `on_command` with source `.tray`, the same shape as window menus. Platforms without a tray-title seam keep the menu updates and log the missing title support once.

For multiple Zig-core items, use `UiApp.Options.status_items_fn`, returning up to eight `App.StatusItemDescriptor` values from `App.StatusItemsScratch`. Descriptor presence creates/removes by `id`; `visible` hides without removing; shell, presentation, and menu hashes reconcile independently. It is mutually exclusive with the singular `status_item` / `status_item_fn` pair.

## The menu-bar app lifecycle

The default TypeScript + Native markup recipe combines one app policy, two window policies, and two commands:

1. Declare the `"tray"` capability and set top-level `dock_visible = false`. macOS selects the Accessory activation policy before creating the startup window, so no Dock tile or cmd+Tab entry flashes. The tray requirement is validated because it becomes the app's route back to hidden windows.
2. Set `initially_hidden = true` on the startup window when the app should launch behind the status item. `dock_visible = false` alone removes desktop presence; it does not suppress the window.
3. Set `close_policy = "hide"`, so the red close button hides the window instead of quitting. The default `"quit"` keeps classic windowed-app behavior; see [Windows](/docs/windows#close-policy).
4. Map tray rows to `Cmd.showWindow("main")` and `Cmd.quitApp()`. Show unhides, orders front, and activates; Quit follows the real graceful shutdown path.

```zig title="app.zon"
.capabilities = .{ "native_views", "gpu_surfaces", "tray" },
.dock_visible = false,
.shell = .{
    .windows = .{
        .{
            .label = "main",
            .initially_hidden = true,
            .close_policy = "hide",
            // views...
        },
    },
},
```

```ts title="src/core.ts"
import { Cmd } from "@native-sdk/core";

export function commandMsg(name: string): Msg | null {
  if (name === "app.open") return { kind: "open_player" };
  if (name === "app.quit") return { kind: "quit" };
  return null;
}

export function update(model: Model, msg: Msg): Model | [Model, Cmd<Msg>] {
  switch (msg.kind) {
    case "open_player":
      return [model, Cmd.showWindow("main")];
    case "quit":
      return [model, Cmd.quitApp()];
    // other arms...
  }
}
```

`Cmd.setDockPresence(true)` can later promote the same running Accessory app to Regular; passing `false` demotes it again. `examples/menu-bar` is the complete zero-Zig loop with `statusItem(model)` supplying the live title and rows.

Linux has no status-item implementation: the toolkit has no status item there yet, so nothing could bring a hidden window back — `close_policy = "hide"` is refused at build/create time with a diagnostic, and the platform-support matrix states it plainly.

A future `.event` close-policy tier (the model receives the close request and decides — unsaved-changes prompts) is deliberately left room for but is not implemented yet; model-declared secondary windows already have that shape today through `WindowDescriptor.on_close`.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Updates
url: "https://native-sdk.dev/docs/updates"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Native SDK includes its own signed updater for packaged macOS apps. It does not use Sparkle or another updater framework.

An enabled app gets:

- **Check for Updates…** in the macOS application menu.
- Optional background checks after launch.
- A standard update-available prompt with release notes.
- Download progress, signed metadata verification, archive size and SHA-256 verification.
- In-place app replacement, rollback when replacement or the relaunch request fails, and relaunch into the new version.

The first backend supports macOS apps using the system host (`web_engine: "system"`). Windows, Linux, and the macOS Chromium host do not install updates yet.

## Configure the app

Generate an Ed25519 key pair once:

```bash
native update keygen --private-key ~/Documents/my-app-update.key
```

The command writes the 32-byte private seed with mode `0600` and prints the base64 public key. Back up the private key securely and keep it out of the app repository, build artifacts, and update server.

Put only the public key and feed URL in `app.json`:

```json
{
  "$schema": "https://schema.native-sdk.dev/app/v1.json",
  "id": "com.example.my-app",
  "name": "my-app",
  "version": "1.0.0",
  "updates": {
    "feed_url": "https://example.com/releases/native-update.json",
    "public_key": "base64-ed25519-public-key",
    "check_on_start": true
  }
}
```

Legacy `app.zon` uses the same fields:

```zig
.updates = .{
    .feed_url = "https://example.com/releases/native-update.json",
    .public_key = "base64-ed25519-public-key",
    .check_on_start = true,
},
```

Both URLs in the release path—the feed URL and archive URL—must use HTTPS. The Ed25519 signature remains the authority if the server or TLS endpoint is compromised.

## Build the update archive

Package the app with the same signing identity as the installed release and request the updater ZIP:

```bash
native build
native package \
  --target macos \
  --signing identity \
  --identity "Developer ID Application: Your Name (TEAMID)" \
  --update-archive
```

The command emits the `.app` and a ZIP named like:

```text
zig-out/package/my-app-1.1.0-macos-ReleaseFast-update.zip
```

That ZIP contains exactly the packaged `.app`. Do not use the user-facing DMG as the update archive. When the manifest enables updates, `zig build package` also requests this updater ZIP automatically; use the direct `native package` command when you need Developer ID signing flags.

## Sign the release feed

Upload the ZIP to its final HTTPS URL, then sign its immutable URL, size, digest, app identity, version, target, and release notes:

```bash
native update sign \
  --manifest app.json \
  --private-key ~/Documents/my-app-update.key \
  --archive zig-out/package/my-app-1.1.0-macos-ReleaseFast-update.zip \
  --url https://example.com/releases/my-app-1.1.0-macos-aarch64.zip \
  --target macos-aarch64 \
  --notes "Faster launch and improved export reliability." \
  --output native-update.json
```

Use `macos-aarch64` for Apple silicon and `macos-x86_64` for Intel. Publish the resulting `native-update.json` at the manifest's `feed_url`. Sign and publish one target-specific feed per app build.

`native update sign` refuses a private key that does not match the public key embedded in the manifest, and refuses an archive whose executable does not contain the architecture named by `--target`.

## Trigger a check

The stock application-menu item requires no app code. `check_on_start` performs a quiet startup check: no-update results stay silent and failures go to the app log.

To place the action in another menu, tray item, shortcut, or native control, dispatch the reserved command:

```text
app.check-for-updates
```

The runtime consumes this command as a host action. Do not map it through a TypeScript core's `commandMsg` or a Zig app's `on_command`.

## Verification model

The updater verifies every release in this order:

1. Decode the bounded feed envelope.
2. Verify its exact payload bytes with the embedded Ed25519 public key.
3. Match bundle ID, target architecture, and a newer semantic version.
4. Download through HTTPS and reject redirects away from HTTPS.
5. Match the signed archive byte size and SHA-256 digest before extraction.
6. Require exactly one top-level `.app` with the expected bundle ID and version.
7. For Developer ID apps, require the update to retain the installed app's bundle identifier and Apple signing team.
8. Exit the app, replace it in the containing directory, retain the old bundle as rollback, and relaunch.

Unsigned and ad-hoc apps can use the Ed25519 trust path for development. Public releases should still be Developer ID signed and notarized so Gatekeeper can identify the developer and validate the downloaded app independently.

## Current limits

- The containing directory must be writable by the current user. The updater does not request administrator privileges or install into an administrator-owned location.
- The macOS system host is supported; Chromium, Windows, and Linux backends are pending.
- Full ZIP releases are supported. Delta updates, channels, phased rollout, skipped-version preferences, and automatic background installation are not implemented yet.
- An app renamed in Finder is supported; the update ZIP may keep its original bundle filename.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Web Engines
url: "https://native-sdk.dev/docs/web-engines"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Apps that [embed web content](/docs/frontend) choose a web engine backend for their WebViews; the app/runtime API is the same across engines. The default is the platform system WebView. On macOS, apps can instead bundle Chromium through CEF. Native-rendered apps carry no web engine at all — this page only applies when a WebView is in the window.

## Support Matrix

<table>
  <thead>
    <tr>
      <th>
        Platform
      </th>

      <th>
        `system`
      </th>

      <th>
        `chromium`
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        macOS
      </td>

      <td>
        WKWebView
      </td>

      <td>
        CEF, bundled with the app
      </td>
    </tr>

    <tr>
      <td>
        Linux
      </td>

      <td>
        WebKitGTK
      </td>

      <td>
        Not wired yet; builds fail early instead of silently using WebKitGTK
      </td>
    </tr>

    <tr>
      <td>
        Windows
      </td>

      <td>
        WebView2
      </td>

      <td>
        Not wired yet; builds fail early instead of silently using WebView2
      </td>
    </tr>
  </tbody>
</table>

Engines share the app API, bridge helpers, and security policy. Use runtime support queries to check host-specific capabilities.

## System WebView

```zig
.web_engine = "system",
```

System mode has no bundled browser dependency. It uses the OS web engine, so rendering and web platform support follow the user's installed OS.

On Windows the system engine is WebView2, whose Evergreen runtime is preinstalled on Windows 11 and current Windows 10; older machines need the runtime installer from the WebView2 download page (`native doctor` checks for it). The build stages the small vendored `WebView2Loader.dll` next to the app executable automatically, and `native package` includes it in the Windows artifact.

## Chromium (CEF)

Set the app engine once:

```zig
.web_engine = "chromium",
.cef = .{ .dir = "third_party/cef/<platform>", .auto_install = false },
```

```bash
native cef install
zig build run
```

macOS supports CEF as an alternative to WKWebView. It bundles a specific Chromium version. Available runtime services differ between hosts; check [Platform Support](/docs/platform-support).

Chromium is available through CEF on macOS. Linux can still use WebKitGTK with `system`, and Windows can use the native system host with `system`; Linux and Windows Chromium builds fail early until their CEF hosts are implemented.

Native-rendered surfaces stay a system-engine feature: the macOS Chromium host does not implement native child views or `gpu_surface` compositing, and those operations report unsupported instead of silently no-oping (see [Platform Support](/docs/platform-support)). Windows, child WebViews, the bridge, keyboard shortcuts, tray, dialogs, clipboard, and app timers behave the same as the system host.

### Setup

Install the runtime through the CLI:

```bash
native cef install
native doctor --manifest app.zon
```

`native cef install` downloads Native SDK's prepared CEF runtime from GitHub Releases, verifies it, and installs a complete layout. App builds currently consume the macOS layout at `third_party/cef/macos`; Linux and Windows layouts are reserved for the future CEF hosts. The prepared runtime already includes `libcef_dll_wrapper`, so app developers do not need CMake or Chromium build knowledge.

The default install uses Native SDK's pinned tested CEF version. Pin that version in your app CI or setup docs with `native cef install --version <version>`, rerun the install when you intentionally update Chromium, then rebuild and repackage the app. A packaged Chromium app must bundle the same CEF layout the binary was linked against; mismatches usually show up as launch failures or missing framework/resource errors.

You can also opt into one-command local setup from the build:

```bash
zig build run
```

Set `.cef = .{ .dir = "third_party/cef/<platform>", .auto_install = true }` in `app.zon` to allow the build/package flow to install the prepared runtime automatically. Manual CEF layouts are still supported with `-Dcef-dir=/path/to/cef` or `.cef.dir`. Advanced users can also install directly from official CEF archives with `native cef install --source official --allow-build-tools`. The expected layout is documented in `third_party/cef/README.md`.

Core maintainers can build CEF from source with `tools/cef/build-from-source.sh` when preparing the first runtime release for a version or testing a new CEF branch before publishing it.

### Build Overrides

<table>
  <thead>
    <tr>
      <th>
        Flag
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `-Dweb-engine=system`
      </td>

      <td>
        Temporarily use the platform system WebView instead of the manifest default.
      </td>
    </tr>

    <tr>
      <td>
        `-Dweb-engine=chromium`
      </td>

      <td>
        Temporarily use CEF on supported platforms. Currently supported for macOS builds.
      </td>
    </tr>

    <tr>
      <td>
        `-Dcef-dir=path`
      </td>

      <td>
        Path to the CEF distribution directory.
      </td>
    </tr>

    <tr>
      <td>
        `-Dcef-auto-install=true`
      </td>

      <td>
        Temporarily opt into running

        `native cef install`

        during Chromium builds when CEF is missing.
      </td>
    </tr>
  </tbody>
</table>

### Bundling

```bash
zig build cef-bundle -Dcef-dir=/path/to/cef
```

Local Chromium builds also copy the CEF framework into `zig-out/bin/Frameworks` when Chromium is selected through `app.zon` or a one-off flag. Packaging includes the runtime when the manifest or CLI override resolves to Chromium.

For beta distribution, verify the packaged `.app` contains `Chromium Embedded Framework.framework` under `Contents/Frameworks` and that signing covers both the app and embedded framework before notarization.

### Smoke Tests

```bash
zig build test-webview-cef-smoke -Dplatform=macos -Dweb-engine=chromium
zig build test-package-cef-layout -Dplatform=macos
```

The Chromium smoke step requires a local CEF layout or `-Dcef-auto-install=true`. It launches the WebView example under CEF with automation, verifies `native.ping` over the bridge, and exercises child WebView create/setFrame/navigate/close. The package layout step verifies that a Chromium macOS package contains the framework and resources CEF expects.

`zig build test-webview-cef-link` builds the WebView example with `-Dweb-engine=chromium` without running it. Use it to check compilation and linking after platform changes. `scripts/gate.sh fast` runs it automatically whenever `src/platform/macos/` files change (macOS only; it skips with a warning when no CEF layout is installed).

## Choosing An Engine

<table>
  <thead>
    <tr>
      <th>
        Consideration
      </th>

      <th>
        System
      </th>

      <th>
        Chromium
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        Bundle size
      </td>

      <td>
        Uses the platform WebView; the loader and platform dependencies still apply.
      </td>

      <td>
        Includes the CEF distribution.
      </td>
    </tr>

    <tr>
      <td>
        Rendering consistency
      </td>

      <td>
        Varies by OS version.
      </td>

      <td>
        Consistent for apps that ship the same CEF build.
      </td>
    </tr>

    <tr>
      <td>
        Best fit
      </td>

      <td>
        Apps that can use the platform’s WebView behavior.
      </td>

      <td>
        Apps that need Chromium behavior, complex frontend stacks, or tighter rendering control.
      </td>
    </tr>
  </tbody>
</table>

## In app.zon

```zig
.web_engine = "system",
// or, on supported platforms:
.web_engine = "chromium",
.cef = .{
  .dir = "third_party/cef/<platform>",
  .auto_install = true,
},
```

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Multiple WebViews
url: "https://native-sdk.dev/docs/webviews"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Apps that [embed web content](/docs/frontend) can model a native window as a stack of named WebViews. A WebView can fill the window, cover a panel-sized region, or sit above another WebView by setting its `frame` and `layer`.

This is the right model for browser chrome, dashboards, previews, auth flows, and any app that needs trusted web UI next to isolated page content. (Canvas-first apps that want one live web pane beside native-rendered widgets should use `web_panes` instead — see [Native Surfaces](/docs/native-surfaces#canvas-webview-in-one-window).)

The startup app source is the default trusted WebView named `main`. Simple apps can ignore this and keep using one page. Advanced apps can resize `main` and place isolated child WebViews beneath or above it. `main` is reserved; child WebViews cannot be created with that label.

## Inspection

macOS system WebViews inherit one inspection capability from their host. Debug
app builds enable it; release builds disable it. The `-Dwebview-inspect` build
option overrides either default for every WebView, independently of `bridge`.
See [macOS WebView inspection](/docs/debugging#macos-webview-inspection) for the
security implications and lower-level host configuration.

## JavaScript API

Enable the built-in WebView helpers from an allowed origin:

```zig
const app_permissions = [_][]const u8{native_sdk.security.permission_window};

.security = .{
    .permissions = &app_permissions,
    .navigation = .{ .allowed_origins = &.{ "zero://app", "https://example.com" } },
},
.js_window_api = true,
```

Then create and manage WebViews from JavaScript:

```javascript
const preview = await window.zero.webviews.create({
  label: "preview",
  url: "https://example.com",
  frame: { x: 24, y: 24, width: 480, height: 320 },
  layer: 10,
  transparent: false,
  bridge: false,
});

await preview.setFrame({ x: 32, y: 32, width: 640, height: 420 });
await preview.navigate("https://example.com/docs");
await preview.setZoom(1.25);
await preview.setLayer(20);
await preview.close();
```

Coordinates are logical content coordinates relative to the parent window: `x: 0, y: 0` is the top-left corner of the window content area.

`layer` controls native stacking. Higher layers appear above lower layers. DOM `z-index` only affects elements inside one WebView; it cannot place one WebView above another.

`transparent: true` requests a transparent native WebView background for chrome and menu surfaces. Support is backend dependent and best effort.

`bridge: true` injects `window.zero` into that WebView. Use it only for trusted app chrome. Leave it `false` for untrusted page content.

`windowId` defaults to the window that calls the command. If you pass it explicitly, it must match the calling window; one window cannot manage another window's WebViews.

`webviews.list()` includes `main` first, followed by open child WebViews in the calling window. `main` reports the current startup WebView frame, layer, bridge state, focus state, and source URL.

You can also enable the helpers with an explicit `builtin_bridge` policy instead of `js_window_api`. In that mode, list every WebView command your app calls: `native-sdk.webview.create`, `native-sdk.webview.list`, `native-sdk.webview.setFrame`, `native-sdk.webview.navigate`, `native-sdk.webview.setZoom`, `native-sdk.webview.setLayer`, and `native-sdk.webview.close`.

## Lifecycle

WebView labels are unique per parent window. Creating a second WebView with the same label fails; close the existing WebView first if you want to replace it.

The `main` WebView can be resized and zoomed through `setFrame({ label: "main", ... })` and `setZoom({ label: "main", ... })` on supported backends. It cannot be closed or replaced. Layering `main` is backend dependent; use child WebView layers for portable stacking.

When a parent window closes, its child WebViews are closed with it. Child WebViews do not receive `window.zero` by default, so untrusted content cannot call native bridge commands through the app bridge.

WebView slots are capped globally per runtime, not per window. The default cap is 16 child WebViews.

## Commands

The helper calls these built-in bridge commands internally:

<table>
  <thead>
    <tr>
      <th>
        Helper
      </th>

      <th>
        Command
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `window.zero.webviews.create(options)`

        /

        `preview`
      </td>

      <td>
        `native-sdk.webview.create`
      </td>

      <td>
        Create a named WebView in a parent window
      </td>
    </tr>

    <tr>
      <td>
        `window.zero.webviews.list()`
      </td>

      <td>
        `native-sdk.webview.list`
      </td>

      <td>
        List WebViews in the calling window
      </td>
    </tr>

    <tr>
      <td>
        `window.zero.webviews.setFrame(options)`

        /

        `preview.setFrame(frame)`
      </td>

      <td>
        `native-sdk.webview.setFrame`
      </td>

      <td>
        Move or resize a WebView
      </td>
    </tr>

    <tr>
      <td>
        `window.zero.webviews.navigate(options)`

        /

        `preview.navigate(url)`
      </td>

      <td>
        `native-sdk.webview.navigate`
      </td>

      <td>
        Navigate a WebView to a new URL
      </td>
    </tr>

    <tr>
      <td>
        `window.zero.webviews.setZoom(options)`

        /

        `preview.setZoom(zoom)`
      </td>

      <td>
        `native-sdk.webview.setZoom`
      </td>

      <td>
        Set page zoom from

        `0.25`

        to

        `5.0`
      </td>
    </tr>

    <tr>
      <td>
        `window.zero.webviews.setLayer(options)`

        /

        `preview.setLayer(layer)`
      </td>

      <td>
        `native-sdk.webview.setLayer`
      </td>

      <td>
        Change native stack order
      </td>
    </tr>

    <tr>
      <td>
        `window.zero.webviews.close(options)`

        /

        `preview.close()`
      </td>

      <td>
        `native-sdk.webview.close`
      </td>

      <td>
        Close a WebView
      </td>
    </tr>
  </tbody>
</table>

WebView URLs are checked against the runtime navigation policy before native views are created or navigated. Add the target origin to `security.navigation.allowed_origins` before loading it.

## Backend Support

System WebView stacks are implemented on macOS and Linux with layers, best-effort transparency, bridge-enabled trusted child WebViews, targeted bridge responses, and page zoom. Windows WebView2 supports child WebViews with these limits: child layers, navigation, and bridge-enabled trusted child WebViews are available when WebView2 is installed, but `main` WebView frame/zoom/layer operations reject with explicit unsupported-backend errors. macOS CEF supports layered child WebViews, page zoom, and bridge-enabled trusted child WebViews. Linux CEF explicitly rejects child WebView creation until native CEF embedding is wired in.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Windows
url: "https://native-sdk.dev/docs/windows"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The Native SDK supports multiple windows. A native-rendered app declares its windows as a scene — `ShellWindow` entries carrying titles, sizes, restore behavior, and view trees, the shape [`UiApp`](/docs/app-model) takes through its `scene` option and the app manifest takes through the [`shell` block](/docs/app-zon#shell):

```zig
const shell_windows = [_]native_sdk.ShellWindow{.{
    .label = "main",
    .title = "My App",
    .width = 480,
    .height = 320,
    .views = &shell_views,
}};
const shell_scene: native_sdk.ShellConfig = .{ .windows = &shell_windows };
```

The first scene window adopts the startup window; additional scene windows are created through the window service. Secondary windows can also be created imperatively from Zig — or, in apps that [embed web content](/docs/frontend), from trusted JavaScript.

Fresh windows use three-way placement. A successful state-store lookup restores the saved frame. Otherwise, an authored `x` or `y` places the window at that origin (constrained to the matching or nearest visible screen on macOS). With neither saved state nor an explicit origin, macOS centers the main window. Under the default `clamp_to_visible_screen` policy, restored frames stay on their matching or nearest display and fresh secondary windows cascade from the active window; `center_on_primary` centers restored and fresh default frames on the primary display. `restore_state = true` is only the persistence opt-in—it no longer makes a first launch look like a restored frame at `(0, 0)`.

## Overlay windows

Window creation accepts four overlay controls:

- `transparent` makes the top-level window and its render surface alpha-capable. A raw scene canvas must also use a non-opaque `gpu_alpha_mode`, and its clear/content alpha determines what reaches the desktop. `UiApp.WindowDescriptor` selects premultiplied alpha and an alpha-zero canvas clear automatically.
- `always_on_top` selects the platform's floating/topmost level.
- `click_through` removes the whole window from pointer hit testing, so clicks, motion, and scrolling reach the window underneath.
- `activate_on_show = false` orders the window front without activating the app or taking keyboard focus. This governs initial creation, the first-present reveal, and `showWindow`; an explicit `focusWindow` remains a deliberate activation request.

The usual heads-up overlay combines all four with a chromeless canvas window:

```zig
const overlay: native_sdk.ShellWindow = .{
    .label = "overlay",
    .titlebar = .chromeless,
    .transparent = true,
    .always_on_top = true,
    .click_through = true,
    .activate_on_show = false,
    .views = &.{.{
        .label = "overlay-canvas",
        .kind = .gpu_surface,
        .fill = true,
        .gpu_alpha_mode = .premultiplied,
    }},
};
```

Canvas windows are created hidden and become visible after their first completed present. A short safety deadline reveals the window if rendering wedges. The overlay flags are installed before either reveal, so no opaque, focus-taking intermediate window flashes on screen.

On macOS these map to an alpha-capable floating `NSWindow`, ignored mouse events, and passive `orderFront`. The system WebView engine can participate in that alpha window; the Chromium engine rejects `transparent` because its windowed CEF browser cannot expose alpha to the parent window. Windows uses a layered topmost window with per-pixel alpha, transparent hit testing, a non-activating initial reveal, and normal activation if the user later clicks an interactive overlay. Its layered presenter composites multiple canvas surfaces in layer order. Because Win32 cannot redirect non-client chrome or child surfaces into the top-level alpha bitmap, a transparent Windows window must use `titlebar = .chromeless`, cannot be combined with application menus, and rejects WebViews and other native views. Linux provides transparency, input regions, and passive show on GTK; topmost is honored on X11 through `_NET_WM_STATE_ABOVE`. Wayland intentionally gives the compositor—not clients—control of topmost placement, so the GTK host reports that limitation and cannot guarantee `always_on_top` there.

## macOS menu-bar and login-item hooks

A TypeScript app does not need an AppKit sidecar for the usual menu-bar-app lifecycle:

- Set `initially_hidden = true` on the startup window to create its native host window ordered out. Unlike canvas present-before-show, no fallback deadline reveals it; `Cmd.showWindow(label)` or an explicit focus is required.
- Set `allows_fullscreen = false` on a settings window to keep ordinary resizing while removing macOS native-fullscreen participation and disabling the green fullscreen/zoom affordance. The flag is fixed when the window is created and is available on manifest windows and `UiApp.WindowDescriptor`.
- Use `Cmd.hideWindow(label)` and `Cmd.showWindow(label)` to hide and restore a live window without destroying its views.
- Use `Cmd.setDockPresence(false)` for an accessory/headless Dock policy and `Cmd.setDockPresence(true)` to return to a regular Dock/app-switcher app.
- Use `Cmd.launchAtLoginStatus(route)` and `Cmd.setLaunchAtLogin(enabled, route)` for `SMAppService.mainApp`. The ok arm receives UTF-8 bytes containing `enabled`, `disabled`, `requires_approval`, or `not_found`; the err arm receives `unsupported`, `failed`, or `invalid_request`. `not_found` normally means the executable is not running from an installed app bundle, while `requires_approval` means the user must approve the item in System Settings.

The login-item calls are keyed effects, so their results record and replay like `Cmd.request`; Dock and window visibility changes are fire-and-forget commands. Older macOS versions without `SMAppService` report `unsupported` instead of requiring weak-linked application code.

## Creating windows from Zig

```zig
const info = try runtime.createWindow(.{
    .label = "tools",
    .title = "Tools",
    .default_frame = native_sdk.geometry.RectF.init(80, 80, 420, 320),
    .initial_placement = .explicit,
});
try runtime.focusWindow(info.id);
```

A transparent imperative window with no explicit `source` is deliberately canvas-only: it does not inherit the app's main WebView source, and hot reload will not add one later. Attach a premultiplied GPU surface to make a Windows-compatible overlay:

```zig
const overlay = try runtime.createWindow(.{
    .label = "overlay",
    .title = "Overlay",
    .default_frame = native_sdk.geometry.RectF.init(80, 80, 420, 160),
    .titlebar = .chromeless,
    .transparent = true,
    .always_on_top = true,
    .click_through = true,
    .activate_on_show = false,
});
_ = try runtime.createView(.{
    .window_id = overlay.id,
    .label = "overlay-canvas",
    .kind = .gpu_surface,
    .frame = native_sdk.geometry.RectF.init(0, 0, 420, 160),
    .gpu_surface = .{ .alpha_mode = .premultiplied },
});
```

Supplying an explicit `source` keeps the WebView-window behavior. Windows rejects that combination when `transparent = true` because a child WebView cannot participate in the layered alpha bitmap.

## Creating windows from JavaScript

`js_window_api` exposes the `window.zero.windows.*` helper in JavaScript. Window commands still require an allowed origin and the `window` permission when runtime permissions are configured:

```zig
const app_permissions = [_][]const u8{native_sdk.security.permission_window};

.security = .{
    .permissions = &app_permissions,
    .navigation = .{ .allowed_origins = &.{ "zero://app" } },
},
.js_window_api = true,
```

Then JavaScript can call the helper from an allowed origin:

```javascript
const win = await window.zero.windows.create({
  label: "tools",
  title: "Tools",
  width: 420,
  height: 320,
});

const all = await window.zero.windows.list();
await window.zero.windows.focus(win.id);
await window.zero.windows.close(win.id);
```

JavaScript creation also accepts `titlebar: "standard" | "hidden_inset" | "hidden_inset_tall" | "chromeless"`. A transparent Windows window requires `"chromeless"`. Omitting `url` makes that transparent window source-less instead of inheriting the calling WebView; a transparent WebView-backed window remains unsupported on Windows.

## Window types

<table>
  <thead>
    <tr>
      <th>
        Type
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `WindowId`
      </td>

      <td>
        Opaque identifier (

        `u64`

        )
      </td>
    </tr>

    <tr>
      <td>
        `WindowCreateOptions`
      </td>

      <td>
        Options for

        `runtime.createWindow()`

        : label, title, frame, and overlay presentation
      </td>
    </tr>

    <tr>
      <td>
        `WindowInfo`
      </td>

      <td>
        Returned after creation: id, label, title, frame
      </td>
    </tr>

    <tr>
      <td>
        `WindowState`
      </td>

      <td>
        Persisted state: id, label, title, frame, open, focused, maximized, fullscreen, scale (the type also carries

        `hidden`

        , deliberately session-transient — it never persists, so every launch starts shown)
      </td>
    </tr>

    <tr>
      <td>
        `WindowRestorePolicy`
      </td>

      <td>
        How macOS places restored and fresh default frames: clamp to the matching/nearest visible screen or center on the primary display
      </td>
    </tr>

    <tr>
      <td>
        `WindowClosePolicy`
      </td>

      <td>
        What the user's close affordance does:

        `quit`

        (the default — really close) or

        `hide`

        (the menu-bar-app shape)
      </td>
    </tr>
  </tbody>
</table>

## Close policy

Each window declares what the user's close affordance (the red traffic light, cmd+W, the caption X) does, via `close_policy`:

- `"quit"` — the default and the classic behavior, unchanged for every existing app: the window really closes, and closing the last one follows the host's exit semantics.
- `"hide"` — the menu-bar/tray-app shape: the close affordance hides the window (it stays alive with its views; `WindowState.hidden` flips true on the frame channel, in the session journal, and in the `hidden` field of the window JSON the `window.zero.windows.*` bridge returns — `open` stays true, so `hidden` is what distinguishes a policy-hidden window from a visible unfocused one) and the app keeps running behind its status item. `Cmd.showWindow(label)` brings it back — a tray "Open" row's natural consequence — and on macOS a visible Dock icon provides a second reopen path. Runtime-initiated closes (`Cmd.closeWindow`, reconcile closes) still really close: the policy governs the USER's affordance, not the app's own decisions.

`UiApp.WindowDescriptor.close_policy` exposes the same enum for model-declared secondary windows. Descriptor presence means the window remains alive: a `.hide` user close retains its slot and views and does not dispatch `on_close`; `showWindow(label)` reveals that same window. Stopping the declaration still performs a real reconcile close.

TypeScript cores expose the same contract as `windows(model): readonly WindowDescriptor[]`, with camel-case `closePolicy`. Each descriptor uses a literal `label: asciiBytes("<label>")` and the matching `src/windows/<label>.native`; check/build reject dynamic labels and missing roots. Those roots may import components nested under `src/windows/`; the main `src/app.native` view can import components anywhere under `src/`. `titlebar` includes `"chromeless"`, including the transparent-Windows shape described above. Under `"quit"`, `onCloseCommand` routes through `commandMsg`; under `"hide"`, no close command fires because the window did not close.

`hide` is supported where the host can re-show a hidden window: macOS (`windowShouldClose` orders the window out) and Windows (`WM_CLOSE` hides via `SW_HIDE`; the tray icon is the ONLY re-show affordance, so on Windows `close_policy = "hide"` additionally requires the `"tray"` capability in the app manifest — hiding removes the taskbar entry and Windows has no Dock-style reopen, so a declaration without the tray is refused at build time and at window create, exactly like Linux's refusal below; and if the declared tray fails to install at runtime, the first hide-close downgrades to a real close with a warning rather than stranding an invisible process). On macOS the Dock reopen path exists only while `dock_visible = true`; an Accessory app has no Dock icon, so its status item must call `Cmd.showWindow(label)` to bring the window back. This is why `dock_visible = false` requires the `"tray"` capability. Linux has no status item in this toolkit yet, so nothing could bring a hidden window back — the declaration is rejected at build time (a compile error for manifest windows) and at window create (`error.UnsupportedWindowClosePolicy`), never a silent no-op. The hidden state is session-transient: it never persists to the window-state store, so every launch starts shown unless the manifest explicitly sets `initially_hidden = true`.

A policy-hidden window is occluded for pacing, exactly like a minimized one: its canvas frame completions drop to the \~1 Hz occluded heartbeat instead of the display grid (a menu-bar app parks hidden for days — full-rate frames there would be pure background CPU burn), and it does not count as a visible display for `.spectrum` emissions. `fx.showWindow` restores full cadence with the window.

See the [tray page's lifecycle recipe](/docs/tray#the-menu-bar-app-lifecycle) and `examples/menu-bar` for the whole pattern. For model-declared secondary windows, use `WindowDescriptor.on_close` to handle close requests.

## Platform limits

<table>
  <thead>
    <tr>
      <th>
        Constant
      </th>

      <th>
        Value
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `max_windows`
      </td>

      <td>
        16
      </td>
    </tr>

    <tr>
      <td>
        `max_window_label_bytes`
      </td>

      <td>
        64
      </td>
    </tr>

    <tr>
      <td>
        `max_window_title_bytes`
      </td>

      <td>
        128
      </td>
    </tr>
  </tbody>
</table>

## Window state persistence

The `window_state.Store` persists geometry to `windows.zon` in the app's state directory. Startup restore uses the window `label` from the manifest and applies the saved frame; titles continue to come from the app manifest or runtime window creation options.

<table>
  <thead>
    <tr>
      <th>
        Field
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `id`
      </td>

      <td>
        Window ID
      </td>
    </tr>

    <tr>
      <td>
        `label`
      </td>

      <td>
        Window label (used for merge matching)
      </td>
    </tr>

    <tr>
      <td>
        `frame`
      </td>

      <td>
        Position and size (x, y, width, height)
      </td>
    </tr>

    <tr>
      <td>
        `maximized`
      </td>

      <td>
        Whether the window was maximized
      </td>
    </tr>

    <tr>
      <td>
        `fullscreen`
      </td>

      <td>
        Whether the window was fullscreen
      </td>
    </tr>

    <tr>
      <td>
        `scale`
      </td>

      <td>
        Display scale factor
      </td>
    </tr>
  </tbody>
</table>

When `saveWindow` is called, the store merges by matching on `label` or `id`, so secondary windows are preserved alongside the main window. Records with missing, malformed, or empty labels are ignored on load and omitted the next time the file is rewritten.

## Declaring windows in app.zon

```zig
.windows = .{
    .{ .label = "main", .title = "native-sdk", .width = 720, .height = 480, .restore_state = true },
},
```

A menu-bar app's window adds the [close policy](#close-policy):

```zig
.windows = .{
    .{ .label = "main", .title = "Player", .width = 420, .height = 260, .close_policy = "hide" },
},
```

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Zig 0.16 Notes
url: "https://native-sdk.dev/docs/zig"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Native SDK uses **Zig 0.16.0** for the toolkit and Zig-core apps. Default apps use [TypeScript and Native markup](/docs/quick-start). `native dev`, `native build`, and `native test` use `zig` from PATH when its version matches; otherwise, they offer to download the pinned, checksum-verified version into `~/.native/toolchains/`. Use `--yes` to accept the download in scripts. `native doctor` reports the available compiler.

Zig 0.16 changed several standard-library APIs. Use this page to resolve errors from code written for earlier versions. The CLI also provides this guide through `native skills get zig`.

<span id="the-one-big-idea" />

## Explicit IO and allocators

Zig 0.16 moved everything that touches the outside world — files, stdout, clocks, sleeping, child processes, sockets — behind an explicit `std.Io` value that you pass in. Your `main` receives one (along with allocators, the environment, and the args) through `std.process.Init`; tests use `std.testing.io`. The second change: containers like `ArrayList` no longer remember their allocator — you pass it to every call.

Inside a Native SDK app, you mostly don't need `std.Io` at all: `update` does files, subprocesses, HTTP, and timers through the [typed effects channel](/docs/state) (`fx.readFile`, `fx.spawn`, `fx.fetch`, `fx.startTimer`), and clock reads go through `fx.wallMs()` or `native_sdk.nowMs()`. Raw `std.Io` shows up in `main`, in tests, and in standalone tools.

## Error-to-fix index

<table>
  <thead>
    <tr>
      <th>
        The compiler says
      </th>

      <th>
        What changed
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `struct 'heap' has no member named 'GeneralPurposeAllocator'`
      </td>

      <td>
        <a href="#main-receives-stdprocessinit">main receives std.process.Init</a>
      </td>
    </tr>

    <tr>
      <td>
        `struct 'fs' has no member named 'cwd'`
      </td>

      <td>
        <a href="#file-io-lives-on-stdiodir">File IO lives on std.Io.Dir</a>
      </td>
    </tr>

    <tr>
      <td>
        `struct 'std' has no member named 'io'`
      </td>

      <td>
        <a href="#printing-and-writers">Printing and writers</a>
      </td>
    </tr>

    <tr>
      <td>
        `struct 'array_list.Aligned(u8,null)' has no member named 'init'`
      </td>

      <td>
        <a href="#arraylist-is-unmanaged">ArrayList is unmanaged</a>
      </td>
    </tr>

    <tr>
      <td>
        `invalid format string 's' for type '...'`
      </td>

      <td>
        <a href="#format-specifiers-t-and-f">Format specifiers <code>\{t}</code> and <code>\{f}</code></a>
      </td>
    </tr>

    <tr>
      <td>
        `struct 'time' has no member named 'sleep'`

        (or

        `'milliTimestamp'`

        )
      </td>

      <td>
        <a href="#time-and-sleeping">Time and sleeping</a>
      </td>
    </tr>

    <tr>
      <td>
        `struct 'process.Child' has no member named 'init'`
      </td>

      <td>
        <a href="#child-processes">Child processes</a>
      </td>
    </tr>

    <tr>
      <td>
        `struct 'process' has no member named 'getEnvMap'`

        (or

        `'argsAlloc'`

        )
      </td>

      <td>
        <a href="#environment-and-arguments">Environment and arguments</a>
      </td>
    </tr>

    <tr>
      <td>
        `struct 'std' has no member named 'net'`
      </td>

      <td>
        Sockets moved to

        `std.Io.net`

        (

        `IpAddress`

        ,

        `Stream`

        ) and take an

        `io`
      </td>
    </tr>

    <tr>
      <td>
        `struct 'fs' has no member named 'selfExePath'`
      </td>

      <td>
        `std.process.executablePath(io, &buf)`

        , returning the length
      </td>
    </tr>

    <tr>
      <td>
        `no field named 'root_source_file' in struct 'Build.ExecutableOptions'`
      </td>

      <td>
        <a href="#buildzig-artifacts-take-modules">build.zig artifacts take modules</a>
      </td>
    </tr>
  </tbody>
</table>

## main receives std.process.Init

There is no allocator boilerplate anymore. `main` takes `std.process.Init`, which carries a leak-checked general-purpose allocator, a process-lifetime arena, the `Io`, the environment, and the command-line arguments:

```zig
pub fn main(init: std.process.Init) !void {
    const gpa = init.gpa;
    const arena = init.arena.allocator();
    const io = init.io;
    const args = try init.minimal.args.toSlice(arena);
    _ = args;
    _ = gpa;
    _ = io;
}
```

The `main` generated by `native init --template zig-core` already has this shape. It passes `init` to the runner and `init.io` to the hot-reload watcher. Helpers that perform IO should accept an `io: std.Io` parameter from `main`.

## File IO lives on std.Io.Dir

`std.fs.cwd()` and `std.fs.File` are gone. The handle is `std.Io.Dir`, and every operation takes `io` right after the receiver. Note the `readFileAlloc` argument order and the explicit size limit:

```zig
const cwd = std.Io.Dir.cwd();
const content = try cwd.readFileAlloc(io, "notes.txt", allocator, .limited(1024 * 1024));
defer allocator.free(content);

try cwd.writeFile(io, .{ .sub_path = "notes.txt", .data = bytes });

var file = try cwd.openFile(io, "notes.txt", .{});
defer file.close(io);
```

Inside `update`, use `fx.readFile` and `fx.writeFile` for user-visible files and receive their results as messages. Use [model persistence](/docs/persistence) or the [record store](/docs/record-store) for app-owned durable state.

## Printing and writers

`std.io.getStdOut().writer()` is gone. Stdout takes `io` plus a buffer you own, printing goes through the `.interface` field, and nothing appears until you flush:

```zig
var stdout_buffer: [4096]u8 = undefined;
var stdout_writer = std.Io.File.stdout().writer(io, &stdout_buffer);
const stdout = &stdout_writer.interface;
defer stdout.flush() catch {};
try stdout.print("{s}\n", .{message});
```

For debug output, `std.debug.print` is unchanged and needs no `io`. `std.io.fixedBufferStream` became `std.Io.Writer.fixed(&buf)` (read the result back with `.buffered()`), and growable string building is `std.Io.Writer.Allocating.init(allocator)`. One-shot formatting keeps `std.fmt.bufPrint` and `std.fmt.allocPrint`, unchanged.

## ArrayList is unmanaged

The list no longer stores its allocator. Start from `.empty` and pass the allocator to every call that can allocate or free:

```zig
var list: std.ArrayList(Habit) = .empty;
defer list.deinit(allocator);
try list.append(allocator, habit);
try list.appendSlice(allocator, more_habits);
const owned = try list.toOwnedSlice(allocator);
```

The old `std.ArrayList(T).init(allocator)` fails with the `no member named 'init'` error above; a managed-style `list.append(item)` fails with `member function expected 2 argument(s), found 1`. (`std.StringHashMap` and `std.AutoHashMap` still have managed `.init(allocator)` forms — only `ArrayList` and explicitly `Unmanaged` types changed.)

## Format specifiers `{t}` and `{f}`

`{s}` formats strings only. Printing an enum or tagged union tag is `{t}`:

```zig
std.debug.print("filter is {t}\n", .{model.filter}); // prints the tag name
```

Custom format methods changed twice over: the old four-parameter signature (`comptime fmt`, `FormatOptions`, `anytype` writer) no longer compiles, and `{}` no longer calls a `format` method — it prints the raw field dump. Declare the new two-parameter signature and print with `{f}`:

```zig
pub fn format(self: Point, writer: *std.Io.Writer) std.Io.Writer.Error!void {
    try writer.print("({d},{d})", .{ self.x, self.y });
}

std.debug.print("{f}\n", .{point});
```

## Time and sleeping

`std.time` keeps only unit constants (`ns_per_ms` and friends); reading clocks and sleeping take `io`:

```zig
try std.Io.sleep(io, std.Io.Duration.fromMilliseconds(50), .awake);
const now_ns: i128 = std.Io.Timestamp.now(io, .real).nanoseconds;
```

App code should not reach for either: `native_sdk.nowMs()` and `native_sdk.monotonicMs()` are the SDK's clock facade, `fx.wallMs()` is the journaled read inside `update`, and repeating work belongs to `fx.startTimer`. See [State & Data Flow](/docs/state).

## Child processes

`Child.init` plus `child.spawn()` collapsed into one call, and lifecycle methods take `io`:

```zig
var child = try std.process.spawn(io, .{
    .argv = &.{ "git", "status", "--porcelain" },
    .stdin = .ignore,
    .stdout = .inherit,
    .stderr = .inherit,
});
const term = try child.wait(io);
```

Inside an app, use `fx.spawn` instead — bounded, replay-friendly, and the exit and output arrive as messages.

## Environment and arguments

`std.process.getEnvMap`, `argsAlloc`, and `argsWithAllocator` are gone; `main` already holds both ends: `init.environ_map.get("HOME")` and `init.minimal.args.toSlice(allocator)`. Building an environment for a child process is `std.process.Environ.Map.init(allocator)` plus `put`.

## build.zig artifacts take modules

Zero-config apps have no `build.zig` — this only matters after `native eject` or `native init --full`. Executables and tests take a `root_module`, and the module owns the root source file, target, and optimize mode:

```zig
const exe = b.addExecutable(.{
    .name = "app",
    .root_module = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    }),
});
```

The ejected `build.zig` the CLI writes is the reference shape.

## Tests get their Io from std.testing.io

Tests never construct an `Io`; `std.testing.io` sits next to `std.testing.allocator`:

```zig
test "round-trips the store" {
    var tmp = std.testing.tmpDir(.{});
    defer tmp.cleanup();
    try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "store.txt", .data = "v1" });
}
```

Model/Msg/update tests — the ones the generated `src/tests.zig` shows — need no `Io` at all.

<span id="one-more-habit-build-and-test-both" />

## Build and test both

Zig analyzes code lazily, so an outdated idiom in a function only tests reference (or only `main` references) can hide from the other command indefinitely. Run both `native build` and `native test` (or `zig build` and `zig build test`) before calling a change done.

## What didn't change

`std.fmt.bufPrint` / `allocPrint` / `parseInt` / `parseFloat`, `std.mem`, `std.debug.print`, `std.heap.page_allocator`, the `std.time` unit constants, `@embedFile`, and `std.testing.expect*` all work as before. If code using only these fails, the problem is elsewhere.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Dev Server
url: "https://native-sdk.dev/docs/cli/dev"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Run `native dev` in an app directory to build and launch the app. It builds Debug by default — the markup hot-reload watcher and the Debug-only diagnostics are compiled in only in Debug; pass `-Doptimize=...` to override (`native build` stays ReleaseFast by default). Native-rendered apps get markup hot reload (edit `src/app.native` while the app runs); no build files are required — apps without a `build.zig` are driven through a CLI-generated build graph, apps with one through `zig build`.

For apps with a frontend dev configuration in their manifest, `native dev` also owns the frontend server lifecycle. It starts the configured frontend process, waits for the port to accept connections, launches the native shell with `NATIVE_SDK_FRONTEND_URL`, sets `NATIVE_SDK_HMR=1`, and terminates the frontend when the shell exits. Framework HMR stays owned by the dev server (Vite, Next.js, etc.) because the WebView loads the dev URL directly.

## Usage

```bash
native dev
native dev path/to/app -Dweb-engine=chromium
native dev --binary zig-out/bin/MyApp
native dev --binary zig-out/bin/MyApp --url http://127.0.0.1:3000/ --command "npm run dev"
native dev --binary zig-out/bin/MyApp --timeout-ms 60000
```

With `--binary`, the build step is skipped and only the frontend dev flow runs against the prebuilt shell (this is what the expanded template's `zig build dev` step invokes).

## Flags

<table>
  <thead>
    <tr>
      <th>
        Flag
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `--manifest`
      </td>

      <td>
        Path to

        `app.json`

        or

        `app.zon`

        (auto-detected, JSON first)
      </td>
    </tr>

    <tr>
      <td>
        `--binary`
      </td>

      <td>
        Path to the built native binary
      </td>
    </tr>

    <tr>
      <td>
        `--url`
      </td>

      <td>
        Override dev server URL from the app manifest
      </td>
    </tr>

    <tr>
      <td>
        `--command`
      </td>

      <td>
        Override dev server command from the app manifest
      </td>
    </tr>

    <tr>
      <td>
        `--timeout-ms`
      </td>

      <td>
        Override readiness timeout (default from the app manifest)
      </td>
    </tr>
  </tbody>
</table>

## Configuration in app.zon

The default scaffold uses `app.json`. The equivalent ZON configuration is:

```zig
.frontend = .{
    .dist = "dist",
    .entry = "index.html",
    .spa_fallback = true,
    .dev = .{
        .url = "http://127.0.0.1:5173/",
        .command = .{ "npm", "run", "dev", "--", "--host", "127.0.0.1" },
        .ready_path = "/",
        .timeout_ms = 30000,
    },
}
```

## Framework recipes

**Vite**: `.url = "http://127.0.0.1:5173/"`, command `npm run dev -- --host 127.0.0.1`.

**Next.js**: `.url = "http://127.0.0.1:3000/"`, command `npm run dev -- --hostname 127.0.0.1`.

**Static preview**: point `.dist` at the build output and use any local server command.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: CLI
url: "https://native-sdk.dev/docs/cli"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The `native` CLI provides project scaffolding, markup validation, automation, packaging, and debugging tools. Every verb accepts `--help` (prints usage and exits 0); unknown flags error with the usage text.

## Core loop

### `native init`

```sh
native init [path] [--template <ts-core|zig-core>] [--frontend <native|next|vite|react|svelte|vue>] [--full]
```

Scaffold a new Native SDK project. The default `native` frontend scaffolds a native-rendered markup app with no web frontend and no build files — the CLI owns the build. The default core is TypeScript: `src/core.ts`, `src/app.native`, `app.json`, and no language flag anywhere else — the build detects which core the tree carries. Omit `path` to scaffold into the current directory.

<dl>
  <dt>
    `--template`
  </dt>

  <dd>
    Which core to scaffold:

    `ts-core`

    (default) for

    `src/core.ts`

    , or

    `zig-core`

    for

    `src/main.zig`

    plus generated full-loop tests. Applies to the native frontend only.
  </dd>

  <dt>
    `--frontend`
  </dt>

  <dd>
    What to scaffold:

    `native`

    (default) for a native-rendered markup app, or

    `next`

    ,

    `vite`

    ,

    `react`

    ,

    `svelte`

    ,

    `vue`

    for a web frontend in a WebView shell.
  </dd>

  <dt>
    `--full`
  </dt>

  <dd>
    Also write

    `build.zig`

    ,

    `build.zig.zon`

    , editor config, and CI.
  </dd>
</dl>

### `native dev`

```sh
native dev [dir]
native dev [dir] --core [--script msgs.ndjson] [--watch]
native dev --binary <path> [--manifest app.json] [--url <url>] [--command "<cmd>"] [--timeout-ms <n>]
```

Build and run the app in the current (or given) app directory — a Debug build by default, printing a one-line completion and naming any failing step. The markup hot-reload watcher and the Debug-only diagnostics are compiled in only in Debug; pass `-Doptimize=...` to override. Apps with a frontend dev config also get the managed dev server — see [Dev Server](/docs/cli/dev).

<dl>
  <dt>
    `--core`
  </dt>

  <dd>
    Run the TypeScript core's logic loop under node instead of building the app: dispatch Msgs as JSON lines on stdin, watch the committed model and effect transcript, advance a virtual clock to fire timers. This mode does not render the UI. Use

    `native dev`

    to run the app in a window. See

    <a href="/docs/typescript#the-dev-loop">TypeScript Cores</a>

    .
  </dd>

  <dt>
    `--script`
  </dt>

  <dd>
    With

    `--core`

    : replay a newline-delimited JSON message file instead of reading stdin.
  </dd>

  <dt>
    `--watch`
  </dt>

  <dd>
    With

    `--core`

    : re-run the script on every

    `src/core.ts`

    edit.
  </dd>

  <dt>
    `--binary`
  </dt>

  <dd>
    Path to a prebuilt app binary — the legacy prebuilt-shell form: the build step is skipped and only the frontend dev flow runs. Ordinary

    `native dev`

    builds the app itself.
  </dd>

  <dt>
    `--manifest`
  </dt>

  <dd>
    Path to

    `app.json`

    or

    `app.zon`

    (default: auto-detected, JSON first).
  </dd>

  <dt>
    `--url`
  </dt>

  <dd>
    Override the dev server URL from the app manifest.
  </dd>

  <dt>
    `--command`
  </dt>

  <dd>
    Override the dev server command (space-separated).
  </dd>

  <dt>
    `--timeout-ms`
  </dt>

  <dd>
    Milliseconds to wait for the dev server (default from the app manifest, or 30000).
  </dd>
</dl>

### `native build`

```sh
native build [dir]
```

Build a ReleaseFast binary into `zig-out/bin/`, printing a one-line completion and naming any failing step.

### `native test`

```sh
native test [dir]
```

Run the app's test suite, printing the zig build summary (step/test tally) plus a final `native test: passed` line. For a Zig core it also refreshes the model-contract artifact `native check` reads.

### `native check`

```sh
native check [dir] [--strict]
```

Validate the whole tree without building the app. A TypeScript core (`src/core.ts`) runs the subset checker first — real tsc semantics plus the app-core rules, diagnostics verbatim — then every `src/**.native` markup file and the app manifest are checked. For a TypeScript core, the views are then compiled against the core with the build's own view compiler, so bindings, iterables, and message tags are always checked, and model state or `Msg` kinds no view uses are reported as warnings. A Zig core checks the same things against a fresh model contract (`zig-out/model-contract.zon`, refreshed by `native test`); without the artifact it degrades to structural checking and says so: "model contract: not yet built - bindings checked structurally only; run `native test` to enable typed checks". Markup accessibility findings are reported per file in full, and a failing `src/*.native` file that no Zig source embeds gets a leftover-file hint.

<dl>
  <dt>
    `--strict`
  </dt>

  <dd>
    Fail on warnings.
  </dd>
</dl>

## App lifecycle

### `native eject`

```sh
native eject [dir]
```

Write an owned `build.zig`/`build.zig.zon` into the app (once); the verbs then drive your files via `zig build`. Ejecting is only for owning the build files — it is never a prerequisite: zero-config apps build, test, and [package](/docs/packaging) directly (`native package` works on the zero-config build as-is).

### `native eject component`

```sh
native eject component <name> [dir]
```

Write an owned copy of a library composite into `src/components/` (once, never overwriting — ejecting again errors with the file to delete first). Ejectable today: `stepper`, `timeline`, `timeline-item` — the library views that are compositions of primitives; engine controls are not on the menu (theme them through [tokens](/docs/theming) instead). TypeScript apps receive Native markup templates through `<import>` and `<use>`; Zig-core apps receive a compatible Zig view when one exists, while components without a Zig form are rejected with a clear message. Each file opens with a header comment walking through the call-site migration, and each builds a widget tree identical to its library form at the moment of ejection. Unknown names get a did-you-mean plus the full ejectable list. See [Building Components](/docs/building-components#use-eject-or-build).

### `native doctor`

```sh
native doctor [--strict] [--manifest app.json] [--web-engine system|chromium] [--cef-dir path] [--cef-auto-install]
```

Check host environment, WebView, manifest, and CEF. See [native doctor](/docs/debugging/doctor) for what each check means.

### `native validate`

```sh
native validate [app.json|app.zon]
```

Validate `app.json` or `app.zon` against the same manifest contract.

### `native package`

```sh
native package [--target <macos|linux|windows|ios|android>] [flags]
```

Package the app for distribution. The manifest is picked up at `app.json` (falling back to `app.zon`) and the binary at `zig-out/bin/<name>` automatically; the flags below override.

<dl>
  <dt>
    `--target`
  </dt>

  <dd>
    Target platform (

    `macos`

    ,

    `linux`

    ,

    `windows`

    ,

    `ios`

    ,

    `android`

    ).
  </dd>

  <dt>
    `--manifest`
  </dt>

  <dd>
    Path to

    `app.json`

    or

    `app.zon`

    .
  </dd>

  <dt>
    `--output`
  </dt>

  <dd>
    Output path for the package.
  </dd>

  <dt>
    `--binary`
  </dt>

  <dd>
    Path to the built binary.
  </dd>

  <dt>
    `--assets`
  </dt>

  <dd>
    Path to frontend assets directory.
  </dd>

  <dt>
    `--optimize`
  </dt>

  <dd>
    Optimization level.
  </dd>

  <dt>
    `--web-engine`
  </dt>

  <dd>
    Temporarily override the app manifest with

    `system`

    or macOS-only

    `chromium`

    .
  </dd>

  <dt>
    `--web-layer`
  </dt>

  <dd>
    Override the app manifest's

    `webview_layer`

    with

    `auto`

    ,

    `include`

    , or

    `exclude`

    — the same precedence as

    `-Dweb-layer`

    in the build graph.

    `zig build package`

    passes the graph's resolved decision here automatically so the package always matches the built executable.
  </dd>

  <dt>
    `--cef-dir`
  </dt>

  <dd>
    Temporarily override the CEF distribution path from the app manifest.
  </dd>

  <dt>
    `--cef-auto-install`
  </dt>

  <dd>
    Temporarily allow prepared CEF installation during Chromium packaging.
  </dd>

  <dt>
    `--signing`
  </dt>

  <dd>
    Signing mode:

    `none`

    ,

    `adhoc`

    , or

    `identity`

    .
  </dd>

  <dt>
    `--identity`
  </dt>

  <dd>
    Code signing identity name.
  </dd>

  <dt>
    `--entitlements`
  </dt>

  <dd>
    Path to entitlements file.
  </dd>

  <dt>
    `--notarize`
  </dt>

  <dd>
    Submit the final macOS artifact to Apple's notary service, then staple and validate the accepted ticket. Requires identity signing.
  </dd>

  <dt>
    `--notary-profile`
  </dt>

  <dd>
    Name of a

    `notarytool`

    Keychain profile created with

    `xcrun notarytool store-credentials`

    .
  </dd>

  <dt>
    `--archive`
  </dt>

  <dd>
    Create a distributable archive. On macOS this is a styled DMG with the app, an Applications alias, a generated or custom background, and the Finder layout declared by the app manifest.
  </dd>
</dl>

### Platform shortcuts

```sh
native package-windows [--output path] [--binary path] [--service-binary path]
native package-linux [--output path] [--binary path] [--service-binary path]
native package-ios [--output path] [--binary path]
native package-android [--output path] [--binary path]
```

Per-platform shortcuts for `native package --target <platform>`.
The desktop shortcuts use an explicit `--service-binary` when supplied; otherwise, service-bearing projects discover the normal `zig-out/bin/<app>_services[.exe]` build output just like the canonical command.

### `native bundle-assets`

```sh
native bundle-assets [app.json|app.zon] [assets] [output]
```

Copy frontend assets into the build output.

## Tooling

### `native markup`

```sh
native markup check <file.native> [more files...] [--strict]
native markup dump <file.native> [--out doc.nsui]
native markup lsp
```

Work with Native markup files directly, outside the app verbs.

<dl>
  <dt>
    `markup check`
  </dt>

  <dd>
    Validate Native markup without building; errors carry

    `file:line:column`

    and a diagnostic message. Run inside an app directory it picks up a fresh model contract the same way

    `native check`

    does;

    `--strict`

    promotes warnings to failures.
  </dd>

  <dt>
    `markup dump`
  </dt>

  <dd>
    Resolve and validate a view, encode the canonical NSUI binary, and print the JSON inspection view derived from the decoded binary;

    `--out`

    also writes the

    `.nsui`

    artifact.
  </dd>

  <dt>
    `markup lsp`
  </dt>

  <dd>
    Run the markup language server (diagnostics, completion, hover) for editor integration.
  </dd>
</dl>

### `native automate`

```sh
native automate <command>
```

Interact with the automation server of a running automation-enabled app. See [Automation](/docs/automation) for the full workflow.

<dl>
  <dt>
    `automate list`
  </dt>

  <dd>
    List running automation-enabled apps.
  </dd>

  <dt>
    `automate snapshot`
  </dt>

  <dd>
    Dump current app state.
  </dd>

  <dt>
    `automate screenshot <view-label> [<scale>]`
  </dt>

  <dd>
    Render a

    `gpu_surface`

    view's canvas to

    `screenshot-<view-label>.png`

    through the deterministic CPU reference renderer.
  </dd>

  <dt>
    `automate reload`
  </dt>

  <dd>
    Reload the WebView.
  </dd>

  <dt>
    `automate resize <width> <height> [<scale>]`
  </dt>

  <dd>
    Dispatch a main-window resize event.
  </dd>

  <dt>
    `automate menu-command <id>`
  </dt>

  <dd>
    Dispatch a menu command event.
  </dd>

  <dt>
    `automate native-command <id> [<view-label>]`
  </dt>

  <dd>
    Dispatch a native view command event.
  </dd>

  <dt>
    `automate widget-action <view-label> <widget-id> <action> [<value>]`
  </dt>

  <dd>
    Invoke a retained canvas widget action.
  </dd>

  <dt>
    `automate widget-click <view-label> <widget-id>`
  </dt>

  <dd>
    Dispatch a pointer click at a retained canvas widget.
  </dd>

  <dt>
    `automate widget-hold <view-label> <widget-id>`
  </dt>

  <dd>
    Press-and-hold a retained canvas widget (drives

    `on_hold`

    ).
  </dd>

  <dt>
    `automate widget-context-press <view-label> <widget-id>`
  </dt>

  <dd>
    Secondary-click a retained canvas widget (context menu, or

    `on_hold`

    when none).
  </dd>

  <dt>
    `automate widget-context-menu <view-label> <widget-id> <item-index>`
  </dt>

  <dd>
    Invoke a widget's declared context-menu item by index — the snapshot lists each widget's items in order.
  </dd>

  <dt>
    `automate widget-drag <view-label> <widget-id> <start-x-ratio> <end-x-ratio> [<start-y-ratio> <end-y-ratio>]`
  </dt>

  <dd>
    Dispatch pointer down/drag/up across a retained canvas widget.
  </dd>

  <dt>
    `automate widget-wheel <view-label> <widget-id> <delta-y> [<delta-x>]`
  </dt>

  <dd>
    Dispatch wheel input at a retained canvas widget; the optional

    `delta-x`

    scrolls the horizontal axis (each axis routes to the nearest region that scrolls it).
  </dd>

  <dt>
    `automate widget-key <view-label> <key> [<text>]`
  </dt>

  <dd>
    Dispatch key input to the focused retained canvas widget.
  </dd>

  <dt>
    `automate widget-pinch <view-label> <scale> [<x> <y>]`
  </dt>

  <dd>
    Dispatch a trackpad pinch gesture at a gpu-surface view (

    `scale`

    is the final multiplicative zoom for the gesture; the anchor point defaults to the view center).
  </dd>

  <dt>
    `automate shortcut <id>`
  </dt>

  <dd>
    Dispatch a shortcut command event.
  </dd>

  <dt>
    `automate tray-action <item-id>`

    /

    `automate tray-action <status-item-id> <item-id>`
  </dt>

  <dd>
    Select a status-item dropdown row; the one-id shorthand targets primary status item

    `#1`

    .
  </dd>

  <dt>
    `automate focus <view-label>`
  </dt>

  <dd>
    Focus a native or WebView-backed view.
  </dd>

  <dt>
    `automate focus-next`

    /

    `automate focus-previous`
  </dt>

  <dd>
    Move focus through visible, enabled views.
  </dd>

  <dt>
    `automate wait`
  </dt>

  <dd>
    Wait for

    `ready=true`

    in the snapshot.
  </dd>

  <dt>
    `automate assert [--absent] [--timeout-ms <n>] <pattern>...`
  </dt>

  <dd>
    Poll the snapshot until every regex pattern matches (or, with

    `--absent`

    , until none match); on timeout, prints the missing patterns plus the snapshot tail and exits non-zero.
  </dd>

  <dt>
    `automate bridge <json>`
  </dt>

  <dd>
    Send a bridge command (origin

    `zero://inline`

    ).
  </dd>
</dl>

### `native skills`

```sh
native skills list
native skills get <name> [--full]
native skills get --all [--full]
```

List and print the built-in AI agent skills the CLI ships — the version-matched content the `npx skills add vercel-labs/native` discovery skill loads from. See [Agent Skills](/docs/skills) for the one-command agent install, what each skill covers, and how to deliver one to an agent.

<dl>
  <dt>
    `skills list`
  </dt>

  <dd>
    List built-in skills served by the installed CLI.
  </dd>

  <dt>
    `skills get <name>`
  </dt>

  <dd>
    Output a skill's content (for example

    `skills get core`

    or

    `skills get native-ui`

    ).
  </dd>

  <dt>
    `skills get --all`
  </dt>

  <dd>
    Output every non-hidden skill.
  </dd>

  <dt>
    `--full`
  </dt>

  <dd>
    Also include a skill's reference and template files when present.
  </dd>
</dl>

### `native cef`

```sh
native cef install|path|doctor [--dir path] [--version version] [--source prepared|official] [--force]
```

Manage the macOS CEF runtime for Chromium-engine apps.

<dl>
  <dt>
    `cef install`
  </dt>

  <dd>
    Download, prepare, and verify the macOS CEF runtime.
  </dd>

  <dt>
    `cef path`
  </dt>

  <dd>
    Print the default or configured CEF directory.
  </dd>

  <dt>
    `cef doctor`
  </dt>

  <dd>
    Check only the CEF layout.
  </dd>
</dl>

<dl>
  <dt>
    `--dir`
  </dt>

  <dd>
    CEF install directory. Defaults to the host platform directory under

    `third_party/cef`

    .
  </dd>

  <dt>
    `--version`
  </dt>

  <dd>
    CEF binary version to download. The default is Native SDK's pinned tested version.
  </dd>

  <dt>
    `--source`
  </dt>

  <dd>
    `prepared`

    or

    `official`

    . Defaults to

    `prepared`

    , which downloads Native SDK's no-CMake runtime from GitHub Releases.
  </dd>

  <dt>
    `--download-url`
  </dt>

  <dd>
    Override the prepared runtime release base URL, or the official CEF host when using

    `--source official`

    .
  </dd>

  <dt>
    `--allow-build-tools`
  </dt>

  <dd>
    Allow the advanced official CEF path to invoke local build tools for

    `libcef_dll_wrapper`

    .
  </dd>

  <dt>
    `--force`
  </dt>

  <dd>
    Redownload and replace the target directory.
  </dd>
</dl>

Core maintainers who need to build CEF before a Native SDK runtime release exists should use `tools/cef/build-from-source.sh`. The CLI's default `native cef install` path remains the no-CMake app-developer path.

### `native version`

```sh
native version
```

Print the native version.

## Environment variables

<dl>
  <dt>
    `NATIVE_SDK_FRONTEND_URL`
  </dt>

  <dd>
    Dev server URL (read by

    `frontend.sourceFromEnv`

    ).
  </dd>

  <dt>
    `NATIVE_SDK_FRONTEND_ASSETS`
  </dt>

  <dd>
    App convention for signaling pre-built assets.
  </dd>

  <dt>
    `NATIVE_SDK_LOG_DIR`
  </dt>

  <dd>
    Override log output directory.
  </dd>

  <dt>
    `NATIVE_SDK_LOG_FORMAT`
  </dt>

  <dd>
    Log format:

    `text`

    or

    `jsonl`

    .
  </dd>
</dl>

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Builtin Commands
url: "https://native-sdk.dev/docs/bridge/builtin-commands"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The Native SDK provides built-in bridge commands for app command routing, window management, generic native views, layered WebViews, platform support queries, native dialogs, selected OS capabilities, and credential storage. These are controlled by the `builtin_bridge` policy in `RuntimeOptions`, separate from app-defined bridge handlers.

## Command Routing

<table>
  <thead>
    <tr>
      <th>
        Command
      </th>

      <th>
        Required permission
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `native-sdk.command.invoke`
      </td>

      <td>
        `command`
      </td>

      <td>
        Dispatch an

        `Event.command`

        into the app runtime from the calling WebView
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.command.list`
      </td>

      <td>
        `command`
      </td>

      <td>
        List the manifest command catalog loaded into the runtime
      </td>
    </tr>
  </tbody>
</table>

Command routing is available through `window.zero.commands.invoke(...)` when `js_window_api` is `true`. The manifest command catalog is available through `window.zero.commands.list()`. When runtime permissions are configured, command helpers require the `command` permission; the legacy `window` permission is still accepted for compatibility. The runtime emits a `CommandEvent` with `source = .bridge`, the calling `window_id`, and the calling view label when the request comes from a named child WebView.

Native controls can also bind a `command` when created with `runtime.createView(...)` or `window.zero.views.create(...)`. On macOS, Linux, and Windows system WebView builds, native button-style controls dispatch the command with the native `window_id` and the originating view label. Controls inside a toolbar report `source = .toolbar`; other native controls report `source = .native_view`.

## Platform Support Commands

<table>
  <thead>
    <tr>
      <th>
        Command
      </th>

      <th>
        Required permission
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `native-sdk.platform.supports`
      </td>

      <td>
        `window`
      </td>

      <td>
        Return whether the current platform and web engine support a feature
      </td>
    </tr>
  </tbody>
</table>

Use `window.zero.platform.supports(...)` when `js_window_api` is enabled. It accepts a `PlatformFeature` name, such as `gpu_surfaces` or `audio_playback`, or a selector object with `feature` or `name`. JavaScript aliases include `gpuSurfaces` and `audioPlayback`. See [Platform Support](/docs/platform-support) for the capability matrix. Use an explicit `builtin_bridge` policy for per-command origin lists.

## Window commands

<table>
  <thead>
    <tr>
      <th>
        Command
      </th>

      <th>
        Required permission
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `native-sdk.window.list`
      </td>

      <td>
        `window`
      </td>

      <td>
        List all open windows
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.window.create`
      </td>

      <td>
        `window`
      </td>

      <td>
        Create a new window
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.window.focus`
      </td>

      <td>
        `window`
      </td>

      <td>
        Focus a window by ID
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.window.close`
      </td>

      <td>
        `window`
      </td>

      <td>
        Close a window by ID
      </td>
    </tr>
  </tbody>
</table>

Window commands are available through `window.zero.windows.*` when `js_window_api` is `true`, but the runtime still checks the request origin and the `window` permission when permissions are configured. Use an explicit `builtin_bridge` policy when you want per-command origin lists.

## View Commands

<table>
  <thead>
    <tr>
      <th>
        Command
      </th>

      <th>
        Required permission
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `native-sdk.view.create`
      </td>

      <td>
        `view`
      </td>

      <td>
        Create a generic native view or WebView-backed view in the calling window
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.view.list`
      </td>

      <td>
        `view`
      </td>

      <td>
        List generic views and WebViews in the calling window
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.view.update`
      </td>

      <td>
        `view`
      </td>

      <td>
        Patch frame, layer, visibility, enabled state, role, text, command, or WebView URL
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.view.setFrame`
      </td>

      <td>
        `view`
      </td>

      <td>
        Move or resize a view
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.view.setVisible`
      </td>

      <td>
        `view`
      </td>

      <td>
        Show or hide a view
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.view.focus`
      </td>

      <td>
        `view`
      </td>

      <td>
        Focus a view when the backend supports native focus for that kind
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.view.focusNext`

        /

        `native-sdk.view.focusPrevious`
      </td>

      <td>
        `view`
      </td>

      <td>
        Move focus through visible, enabled native controls and WebView-backed views
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.view.close`
      </td>

      <td>
        `view`
      </td>

      <td>
        Close a generic view or child WebView
      </td>
    </tr>
  </tbody>
</table>

View commands are available through `window.zero.views.*` when `js_window_api` is `true`. They use origin checks and the `view` permission when runtime permissions are configured; the legacy `window` permission is still accepted for compatibility. `windowId` must match the calling window when provided. The `update(label, patch)`, `focus(label)`, and `close(label)` helpers accept string labels for the calling window; pass selector objects when you need an explicit `windowId`. View responses include frame, layer, visibility, enabled, focus, role, accessibility label, text, command, cursor, and open state. GPU responses also include surface presentation fields, input latency budget fields, retained canvas frame counters, and retained widget counters. `kind: "webview"` routes through the WebView backend. The macOS, Linux, and Windows system-WebView backends support native `toolbar`, `titlebar_accessory`, `sidebar`, `statusbar`, `split`, `stack`, `button`, `icon_button`, `list_item`, `checkbox`, `toggle`, `segmented_control`, `text_field`, `search_field`, `label`, `spacer`, and `progress_indicator` views. The macOS, Linux, and Windows system hosts also support `gpu_surface`. Chromium hosts and unsupported native kinds return explicit unsupported-backend errors.

## WebView Commands

<table>
  <thead>
    <tr>
      <th>
        Command
      </th>

      <th>
        Required permission
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `native-sdk.webview.create`
      </td>

      <td>
        `window`
      </td>

      <td>
        Create a named WebView in a window
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.webview.list`
      </td>

      <td>
        `window`
      </td>

      <td>
        List WebViews in the calling window
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.webview.setFrame`
      </td>

      <td>
        `window`
      </td>

      <td>
        Move or resize a WebView
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.webview.navigate`
      </td>

      <td>
        `window`
      </td>

      <td>
        Navigate a WebView
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.webview.setZoom`
      </td>

      <td>
        `window`
      </td>

      <td>
        Set page zoom from

        `0.25`

        to

        `5.0`
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.webview.setLayer`
      </td>

      <td>
        `window`
      </td>

      <td>
        Change native stack order
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.webview.close`
      </td>

      <td>
        `window`
      </td>

      <td>
        Close a WebView
      </td>
    </tr>
  </tbody>
</table>

WebView commands are available through `window.zero.webviews.*` when `js_window_api` is `true`. They use the same origin and `window` permission checks as window commands. WebView URLs are also checked against `security.navigation.allowed_origins`. Backend-specific gaps reject with `invalid_request` and an actionable unsupported-backend message.

If `windowId` is omitted, the runtime uses the window that sent the bridge message. If provided, `windowId` must match that same calling window. Child WebViews are isolated by default and receive `window.zero` only when created with `bridge: true`. The startup WebView is always listed as `main`; that label is reserved and cannot be used for child WebView creation.

## Dialog commands

<table>
  <thead>
    <tr>
      <th>
        Command
      </th>

      <th>
        Required permission
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `native-sdk.dialog.openFile`
      </td>

      <td>
        `dialog`
      </td>

      <td>
        Show a file open dialog
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.dialog.saveFile`
      </td>

      <td>
        `dialog`
      </td>

      <td>
        Show a file save dialog
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.dialog.showMessage`
      </td>

      <td>
        `dialog`
      </td>

      <td>
        Show a message dialog
      </td>
    </tr>
  </tbody>
</table>

Dialog commands are **always default-deny** and require an explicit `builtin_bridge` policy.

## OS Commands

<table>
  <thead>
    <tr>
      <th>
        Command
      </th>

      <th>
        Required permission
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `native-sdk.os.openUrl`
      </td>

      <td>
        `network`
      </td>

      <td>
        Open an allowed

        `http://`

        or

        `https://`

        URL in the system browser
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.os.showNotification`
      </td>

      <td>
        `notifications`
      </td>

      <td>
        Show or replace a native notification; optional paired

        `actionLabel`

        /

        `actionCommand`

        fields dispatch an app command
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.os.revealPath`
      </td>

      <td>
        `filesystem`
      </td>

      <td>
        Reveal a local file or folder path in the platform file manager
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.os.addRecentDocument`
      </td>

      <td>
        `filesystem`
      </td>

      <td>
        Add a local path to the platform recent documents list
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.os.clearRecentDocuments`
      </td>

      <td>
        `filesystem`
      </td>

      <td>
        Clear recent documents registered by the application where the platform supports it
      </td>
    </tr>
  </tbody>
</table>

OS commands are **always default-deny** and require an explicit `builtin_bridge` policy. `native-sdk.os.openUrl` also checks `security.navigation.external_links`; the URL must match the external link allowlist before the platform service is called. An actionable notification additionally requires an allowed `native-sdk.command.invoke` policy and the `command` permission for the same origin, because activating it enters the normal app-command path. macOS, Linux, and Windows system WebView hosts implement `openUrl`, `revealPath`, notifications, and recent-document commands; macOS Chromium also implements the current OS command set. Other platform hosts return `invalid_request` with the standard unsupported-service message until implemented.

## Credential Commands

<table>
  <thead>
    <tr>
      <th>
        Command
      </th>

      <th>
        Required permission
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `native-sdk.credentials.set`
      </td>

      <td>
        `credentials`
      </td>

      <td>
        Store a secret by

        `service`

        and

        `account`
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.credentials.get`
      </td>

      <td>
        `credentials`
      </td>

      <td>
        Read a secret by

        `service`

        and

        `account`

        , returning

        `null`

        when it is missing
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.credentials.delete`
      </td>

      <td>
        `credentials`
      </td>

      <td>
        Delete a stored secret by

        `service`

        and

        `account`
      </td>
    </tr>
  </tbody>
</table>

Credential commands are **always default-deny** and require an explicit `builtin_bridge` policy. The macOS system WebView and macOS Chromium hosts store secrets in Keychain. The Linux system WebView host stores secrets through Secret Service/libsecret when available. The Windows system WebView host stores secrets in Credential Manager. Other platform hosts return `invalid_request` with the standard unsupported-service message until implemented.

## Clipboard Commands

<table>
  <thead>
    <tr>
      <th>
        Command
      </th>

      <th>
        Required permission
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `native-sdk.clipboard.readText`
      </td>

      <td>
        `clipboard`
      </td>

      <td>
        Read

        `text/plain`

        from the system clipboard
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.clipboard.writeText`
      </td>

      <td>
        `clipboard`
      </td>

      <td>
        Write

        `text/plain`

        to the system clipboard
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.clipboard.read`
      </td>

      <td>
        `clipboard`
      </td>

      <td>
        Read clipboard data by MIME type and return

        <code>
          \{ mimeType, data }
        </code>
      </td>
    </tr>

    <tr>
      <td>
        `native-sdk.clipboard.write`
      </td>

      <td>
        `clipboard`
      </td>

      <td>
        Write clipboard data by MIME type
      </td>
    </tr>
  </tbody>
</table>

Clipboard commands are **always default-deny** and require an explicit `builtin_bridge` policy. Supported clipboard hosts provide `text/plain` through the existing text clipboard path. macOS system WebView, macOS Chromium, Linux system WebView, and Windows system WebView also support `text/html`, `text/rtf`, and `application/rtf`.

## Enabling builtin commands

```zig
const app_permissions = [_][]const u8{
    native_sdk.security.permission_command,
    native_sdk.security.permission_view,
    native_sdk.security.permission_dialog,
    native_sdk.security.permission_window,
    native_sdk.security.permission_network,
    native_sdk.security.permission_filesystem,
    native_sdk.security.permission_clipboard,
    native_sdk.security.permission_notifications,
    native_sdk.security.permission_credentials,
};

.security = .{
    .permissions = &app_permissions,
    .navigation = .{
        .allowed_origins = &.{ "zero://app" },
        .external_links = .{
            .action = .open_system_browser,
            .allowed_urls = &.{ "https://example.com/docs/*" },
        },
    },
},
.builtin_bridge = .{
    .enabled = true,
    .commands = &.{
        .{ .name = "native-sdk.window.list", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.command.invoke", .permissions = .{ "command" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.command.list", .permissions = .{ "command" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.platform.supports", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.window.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.view.create", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.view.list", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.view.update", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.view.setFrame", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.view.setVisible", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.view.focus", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.view.focusNext", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.view.focusPrevious", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.view.close", .permissions = .{ "view" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.webview.create", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.webview.list", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.webview.setFrame", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.webview.navigate", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.webview.setZoom", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.webview.setLayer", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.webview.close", .permissions = .{ "window" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.dialog.openFile", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.dialog.saveFile", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.dialog.showMessage", .permissions = .{ "dialog" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.openUrl", .permissions = .{ "network" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.showNotification", .permissions = .{ "notifications" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.revealPath", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.addRecentDocument", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.os.clearRecentDocuments", .permissions = .{ "filesystem" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.clipboard.readText", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.clipboard.writeText", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.clipboard.read", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.clipboard.write", .permissions = .{ "clipboard" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.credentials.set", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.credentials.get", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } },
        .{ .name = "native-sdk.credentials.delete", .permissions = .{ "credentials" }, .origins = .{ "zero://app" } },
    },
},
```

## JavaScript usage

Window creation accepts `titlebar` (`standard`, `hidden_inset`, `hidden_inset_tall`, or `chromeless`) plus the overlay fields `transparent`, `alwaysOnTop`, `clickThrough`, and `activateOnShow`. A transparent create without `url` stays source-less instead of inheriting the app WebView; transparent Windows windows also require `titlebar: "chromeless"` and cannot host WebView content.

```javascript
await window.zero.windows.create({
  label: "tools",
  title: "Tools",
  width: 420,
  height: 320,
});

const preview = await window.zero.webviews.create({
  label: "preview",
  url: "https://example.com",
  frame: { x: 24, y: 24, width: 480, height: 320 },
  layer: 10,
  bridge: false,
});

await preview.setZoom(1.25);
await preview.setLayer(20);
await preview.close();

const toolbar = await window.zero.views.create({
  label: "toolbar",
  kind: "toolbar",
  frame: { x: 0, y: 0, width: 720, height: 44 },
  role: "toolbar",
});

await window.zero.views.create({
  label: "refresh",
  kind: "button",
  parent: "toolbar",
  frame: { x: 12, y: 8, width: 96, height: 28 },
  accessibilityLabel: "Refresh workspace",
  text: "Refresh",
  command: "app.refresh",
});

await toolbar.setVisible(false);

await window.zero.commands.invoke("app.save");
const commands = await window.zero.commands.list();

const hasNativeViews = await window.zero.platform.supports("native_views");

const files = await window.zero.invoke("native-sdk.dialog.openFile", {
  title: "Select a file",
  allowMultiple: true,
});

const result = await window.zero.invoke("native-sdk.dialog.showMessage", {
  style: "warning",
  title: "Confirm",
  message: "Are you sure?",
  primaryButton: "Yes",
  secondaryButton: "No",
});

await window.zero.os.openUrl("https://example.com/docs/start");
await window.zero.os.showNotification({
  id: "build-status",
  title: "Build finished",
  subtitle: "native-sdk",
  body: "All checks passed.",
  actionLabel: "Open results",
  actionCommand: "build.open-results",
});
await window.zero.os.revealPath("/Users/me/Downloads/report.pdf");
await window.zero.os.addRecentDocument("/Users/me/Downloads/report.pdf");
await window.zero.os.clearRecentDocuments();

await window.zero.clipboard.writeText("Copied from Native SDK");
const text = await window.zero.clipboard.readText();
await window.zero.clipboard.write({
  mimeType: "text/html",
  data: "<strong>Copied from Native SDK</strong>",
});
const html = await window.zero.clipboard.read({ mimeType: "text/html" });

await window.zero.credentials.set({
  service: "com.example.app",
  account: "current-user",
  secret: "session-token",
});
const token = await window.zero.credentials.get({
  service: "com.example.app",
  account: "current-user",
});
await window.zero.credentials.delete({
  service: "com.example.app",
  account: "current-user",
});
```

See also: [Multiple WebViews](/docs/webviews) for frame and layer semantics, [Dialogs](/docs/dialogs) for the full dialog type reference, [Capabilities](/docs/capabilities) for OS capability support, and [Security](/docs/security) for policy details.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Bridge
url: "https://native-sdk.dev/docs/bridge"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

For apps that [embed web content](/docs/frontend), the bridge connects JavaScript in the WebView to native Zig handlers via JSON messages. Native-rendered apps have no bridge — markup dispatches typed messages straight into `update` (see [App Model](/docs/app-model)).

## Architecture

```
WebView JS                       Zig Runtime
──────────                       ───────────
window.zero.invoke(cmd, payload)
        │                              │
        ├──── JSON message ───────────►│
        │                         Size check (16 KiB max)
        │                         Policy check (origin + permissions)
        │                         Handler lookup + execute
        │◄─── JSON response ──────────┤
```

## Defining a handler

```zig
fn ping(context: *anyopaque, invocation: native_sdk.bridge.Invocation, output: []u8) anyerror![]const u8 {
    _ = invocation;
    const self: *App = @ptrCast(@alignCast(context));
    self.ping_count += 1;
    return std.fmt.bufPrint(output, "{{\"message\":\"pong\",\"count\":{d}}}", .{self.ping_count});
}
```

The handler writes its JSON result into the provided `output` buffer (max 12 KiB) and returns a slice of it. Results must be valid JSON values; invalid raw text is rejected with `handler_failed`. When returning user data as a string, use the bridge helper so quotes and control characters are escaped:

```zig
return native_sdk.bridge.writeJsonStringValue(output, user_supplied_name);
```

## Wiring the dispatcher

```zig
fn bridge(self: *App) native_sdk.BridgeDispatcher {
    self.handlers = .{.{ .name = "native.ping", .context = self, .invoke_fn = ping }};
    return .{
        .policy = .{ .enabled = true, .commands = &policies },
        .registry = .{ .handlers = &self.handlers },
    };
}
```

## Calling from JavaScript

```javascript
const result = await window.zero.invoke("native.ping", { source: "webview" });
console.log(result); // { message: "pong from Zig", count: 1 }
```

## Invocation

When a handler is called, it receives an `Invocation` with:

- `request.id` -- caller-provided request ID (max 64 bytes)
- `request.command` -- command name (max 128 bytes, no `/` or spaces)
- `request.payload` -- JSON payload string
- `source.origin` -- origin of the requesting page (e.g. `zero://app`)
- `source.window_id` -- which window sent the request

## Size limits

<table>
  <thead>
    <tr>
      <th>
        Constant
      </th>

      <th>
        Value
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `max_message_bytes`
      </td>

      <td>
        16 KiB
      </td>
    </tr>

    <tr>
      <td>
        `max_response_bytes`
      </td>

      <td>
        16 KiB
      </td>
    </tr>

    <tr>
      <td>
        `max_result_bytes`
      </td>

      <td>
        12 KiB
      </td>
    </tr>

    <tr>
      <td>
        `max_id_bytes`
      </td>

      <td>
        64
      </td>
    </tr>

    <tr>
      <td>
        `max_command_bytes`
      </td>

      <td>
        128
      </td>
    </tr>
  </tbody>
</table>

## Error codes

When a bridge call fails, the JS promise rejects with an error containing a `code` field:

<table>
  <thead>
    <tr>
      <th>
        Code
      </th>

      <th>
        Cause
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `invalid_request`
      </td>

      <td>
        Malformed JSON message
      </td>
    </tr>

    <tr>
      <td>
        `unknown_command`
      </td>

      <td>
        No handler registered
      </td>
    </tr>

    <tr>
      <td>
        `permission_denied`
      </td>

      <td>
        Origin or permission check failed
      </td>
    </tr>

    <tr>
      <td>
        `handler_failed`
      </td>

      <td>
        Handler returned an error
      </td>
    </tr>

    <tr>
      <td>
        `payload_too_large`
      </td>

      <td>
        Message exceeds 16 KiB
      </td>
    </tr>

    <tr>
      <td>
        `internal_error`
      </td>

      <td>
        Unexpected runtime error
      </td>
    </tr>
  </tbody>
</table>

```javascript
try {
  const result = await window.zero.invoke("native.ping", {});
} catch (error) {
  console.error(error.code, error.message);
}
```

## Bridge types

<table>
  <thead>
    <tr>
      <th>
        Type
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `BridgeDispatcher`
      </td>

      <td>
        Combines policy and registry
      </td>
    </tr>

    <tr>
      <td>
        `BridgePolicy`
      </td>

      <td>
        Whether the bridge is enabled and which commands are allowed
      </td>
    </tr>

    <tr>
      <td>
        `BridgeCommandPolicy`
      </td>

      <td>
        Per-command:

        `name`

        ,

        `permissions`

        ,

        `origins`
      </td>
    </tr>

    <tr>
      <td>
        `BridgeRegistry`
      </td>

      <td>
        Maps command names to handler functions
      </td>
    </tr>

    <tr>
      <td>
        `BridgeHandler`
      </td>

      <td>
        `name`

        ,

        `context`

        ,

        `invoke_fn`
      </td>
    </tr>
  </tbody>
</table>

See also: [Builtin Commands](/docs/bridge/builtin-commands) for `native-sdk.command.*`, `native-sdk.window.*`, `native-sdk.view.*`, `native-sdk.webview.*`, `native-sdk.dialog.*`, `native-sdk.os.*`, `native-sdk.clipboard.*`, and `native-sdk.credentials.*`.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: native doctor
url: "https://native-sdk.dev/docs/debugging/doctor"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The `native doctor` command checks your development environment for issues.

## What it checks

<table>
  <thead>
    <tr>
      <th>
        Check
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        Host platform
      </td>

      <td>
        Operating system and architecture
      </td>
    </tr>

    <tr>
      <td>
        WebView
      </td>

      <td>
        WKWebView (macOS) or WebKitGTK (Linux) availability
      </td>
    </tr>

    <tr>
      <td>
        Manifest
      </td>

      <td>
        `app.json`

        or

        `app.zon`

        validation when a manifest is discovered or passed explicitly
      </td>
    </tr>

    <tr>
      <td>
        Log directory
      </td>

      <td>
        Writability of the log output path
      </td>
    </tr>

    <tr>
      <td>
        CEF
      </td>

      <td>
        CEF distribution presence when Chromium is selected by the app manifest or

        `--web-engine chromium`
      </td>
    </tr>

    <tr>
      <td>
        Signing tools
      </td>

      <td>
        Code signing tool availability
      </td>
    </tr>
  </tbody>
</table>

## Usage

```bash
# Informational (always exits 0)
native doctor

# Strict mode (exits non-zero on any warning)
native doctor --manifest app.json --strict

# Check CEF setup
native doctor --manifest app.json
```

## Flags

<table>
  <thead>
    <tr>
      <th>
        Flag
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `--strict`
      </td>

      <td>
        Exit non-zero on any warning
      </td>
    </tr>

    <tr>
      <td>
        `--manifest`
      </td>

      <td>
        Path to

        `app.json`

        or

        `app.zon`
      </td>
    </tr>

    <tr>
      <td>
        `--web-engine`
      </td>

      <td>
        Temporarily override the engine from the app manifest with

        `system`

        or

        `chromium`
      </td>
    </tr>

    <tr>
      <td>
        `--cef-dir`
      </td>

      <td>
        Temporarily override the CEF distribution path
      </td>
    </tr>

    <tr>
      <td>
        `--cef-auto-install`
      </td>

      <td>
        Temporarily allow automatic prepared CEF installation for Chromium checks
      </td>
    </tr>
  </tbody>
</table>

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Debugging
url: "https://native-sdk.dev/docs/debugging"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The Native SDK provides structured tracing, persistent logging, panic capture, and diagnostic tools for debugging desktop apps.

## macOS WebView inspection

For the macOS system WebView backend, Debug app builds enable Safari inspection,
developer extras, and the default **Toggle Web Inspector** menu item. ReleaseFast,
ReleaseSafe, and ReleaseSmall builds disable them, including the separate release
binary produced by `zig build package` in an expanded build. This applies to
primary WebViews, secondary windows, and child WebViews, including ones created
later by a native-rendered app.

Override the default with a build option:

```bash
native dev -Dwebview-inspect=false
native build -Dwebview-inspect=true
```

Apps that own their build can pass the same option to `zig build` or
`zig build package`. Enabling inspection in a release build allows an attached
Safari console to execute JavaScript in the page's origin, including its allowed
native bridge commands. Keep it disabled for normal distribution.

Lower-level hosts initialized directly through `AppInfo` default to inspection
disabled; set `webview_inspection = true` explicitly for debugging. This setting
covers WKWebView on macOS and does not configure Chromium or other platforms.

## Trace modes

The runtime emits structured trace records. Control verbosity with `TraceMode`:

<table>
  <thead>
    <tr>
      <th>
        Mode
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `off`
      </td>

      <td>
        No trace output
      </td>
    </tr>

    <tr>
      <td>
        `events`
      </td>

      <td>
        Lifecycle and platform events only (default)
      </td>
    </tr>

    <tr>
      <td>
        `runtime`
      </td>

      <td>
        Runtime internals: frame timing, invalidation, window state
      </td>
    </tr>

    <tr>
      <td>
        `all`
      </td>

      <td>
        Everything
      </td>
    </tr>
  </tbody>
</table>

Enable at build time with `-Dtrace=true`, or parse from a string:

```zig
const mode = native_sdk.debug.parseTraceMode("all"); // returns ?TraceMode
```

## Trace sinks

Trace records are routed through sinks. Native SDK provides three:

**FileTraceSink** -- appends records to a file on disk:

```zig
var file_sink = native_sdk.debug.FileTraceSink.init(io, log_dir, log_file, .json_lines);
```

**FanoutTraceSink** -- broadcasts to multiple child sinks (e.g. stdout + file):

```zig
var sinks = [_]trace.Sink{ stdout_sink.sink(), file_sink.sink() };
var fanout = native_sdk.debug.FanoutTraceSink{ .sinks = &sinks };
```

**StdoutTraceSink** (from Native SDK's trace module) -- writes to stdout for interactive development.

Wire a sink into the runtime via `RuntimeOptions.trace_sink`:

```zig
var runtime = native_sdk.Runtime.init(.{
    .platform = my_platform,
    .trace_sink = fanout.sink(),
});
```

## Log format

The `NATIVE_SDK_LOG_FORMAT` environment variable controls the persistent log format:

<table>
  <thead>
    <tr>
      <th>
        Value
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `jsonl`
      </td>

      <td>
        JSON Lines -- one JSON object per trace record (default)
      </td>
    </tr>

    <tr>
      <td>
        `text`
      </td>

      <td>
        Human-readable text lines
      </td>
    </tr>
  </tbody>
</table>

## Log paths

Default log file locations by platform:

<table>
  <thead>
    <tr>
      <th>
        Platform
      </th>

      <th>
        Path
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        macOS
      </td>

      <td>
        `~/Library/Logs/<bundle-id>/native-sdk.jsonl`
      </td>
    </tr>

    <tr>
      <td>
        Linux
      </td>

      <td>
        `~/.local/state/<bundle-id>/logs/native-sdk.jsonl`
      </td>
    </tr>

    <tr>
      <td>
        Windows
      </td>

      <td>
        `%LOCALAPPDATA%&lt;bundle-id>\Logs\native-sdk.jsonl`
      </td>
    </tr>
  </tbody>
</table>

Override with `NATIVE_SDK_LOG_DIR`:

```bash
NATIVE_SDK_LOG_DIR=/tmp/my-logs native dev
```

## Panic capture

The Native SDK captures Zig panics before the default handler runs:

1. Writes a report to `last-panic.txt` in the log directory (includes panic message and return address)
2. Appends a `fatal` trace record to the log file
3. Invokes `std.debug.defaultPanic` for the normal Zig panic output

Enable in your app:

```zig
pub const panic = std.debug.FullPanic(native_sdk.debug.capturePanic);
```

Then call `installPanicCapture` during startup:

```zig
native_sdk.debug.installPanicCapture(io, log_setup.paths);
```

## Debug overlay

For apps with [web content](/docs/frontend), build with `-Ddebug-overlay=true` to enable a visual debugging overlay in the WebView. This shows frame timing, invalidation regions, and window metadata.

```bash
native dev -Ddebug-overlay=true
```

See also [native doctor](/docs/debugging/doctor) for a full diagnostic tool reference.

## Environment variables

<table>
  <thead>
    <tr>
      <th>
        Variable
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `NATIVE_SDK_LOG_DIR`
      </td>

      <td>
        Override log output directory
      </td>
    </tr>

    <tr>
      <td>
        `NATIVE_SDK_LOG_FORMAT`
      </td>

      <td>
        Log format:

        `text`

        or

        `jsonl`

        (default:

        `jsonl`

        )
      </td>
    </tr>
  </tbody>
</table>

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Packaging
url: "https://native-sdk.dev/docs/packaging"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The Native SDK provides tooling to bundle your app into distributable packages for macOS, Linux, and Windows. A native-rendered app packages as a single binary plus icons, metadata, and whatever lives in your `assets/` directory — no browser runtime. On macOS the asset tree is mirrored into the bundle at its app-relative path (`Contents/Resources/assets/`), and the packaged app resolves relative asset paths (audio files, bundled fonts) against `Contents/Resources`, so a path like `assets/music/track.mp3` names the same file in a dev run and in the installed app. Keep large optional data out of `assets/` when you package, or it ships. The frontend-asset and CEF sections below apply only to apps that [embed web content](/docs/frontend); Chromium packaging is currently supported for macOS and includes the CEF runtime when `"web_engine": "chromium"` and the matching CEF layout is installed.

## Quick start

Build and package in two steps:

```bash
native build
native package --target macos
```

`native package` picks up `app.json` (falling back to `app.zon`) and the binary at `zig-out/bin/<name>` automatically; pass `--manifest`, `--binary`, or the other flags below for more control. Zero-config apps package without ejecting: the two commands above are the complete path from source to distributable, and `native eject` exists only for apps that want to own their build files. Apps that do own their build ([ejected](/docs/cli) or scaffolded with `--full`) also get a `zig build package` step that wires the same thing into the build graph.

## Build options

The build system exposes options that control platform, web engine, and build features:

<table>
  <thead>
    <tr>
      <th>
        Option
      </th>

      <th>
        Values
      </th>

      <th>
        Default
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `-Dplatform`
      </td>

      <td>
        `auto`

        ,

        `null`

        ,

        `macos`

        ,

        `linux`

        ,

        `windows`
      </td>

      <td>
        `auto`
      </td>

      <td>
        Target platform
      </td>
    </tr>

    <tr>
      <td>
        `-Dweb-engine`
      </td>

      <td>
        `system`

        ,

        `chromium`
      </td>

      <td>
        App manifest
      </td>

      <td>
        Temporary WebView engine override; Chromium is currently macOS-only
      </td>
    </tr>

    <tr>
      <td>
        `-Dcef-dir`
      </td>

      <td>
        path
      </td>

      <td>
        \--
      </td>

      <td>
        Temporary CEF distribution directory override
      </td>
    </tr>

    <tr>
      <td>
        `-Dtrace`
      </td>

      <td>
        `off`

        ,

        `events`

        ,

        `runtime`

        ,

        `all`
      </td>

      <td>
        `events`
      </td>

      <td>
        Trace output level
      </td>
    </tr>

    <tr>
      <td>
        `-Ddebug-overlay`
      </td>

      <td>
        `true`

        ,

        `false`
      </td>

      <td>
        `false`
      </td>

      <td>
        Enable debug overlay in WebView
      </td>
    </tr>

    <tr>
      <td>
        `-Dautomation`
      </td>

      <td>
        `true`

        ,

        `false`
      </td>

      <td>
        `false`
      </td>

      <td>
        Enable automation server
      </td>
    </tr>

    <tr>
      <td>
        `-Djs-bridge`
      </td>

      <td>
        `true`

        ,

        `false`
      </td>

      <td>
        `false`
      </td>

      <td>
        Enable JavaScript bridge
      </td>
    </tr>
  </tbody>
</table>

## App manifest packaging fields

The manifest drives packaging metadata:

```json title="app.json"
{
  "id": "com.example.myapp",
  "name": "myapp",
  "display_name": "My App",
  "version": "1.0.0",
  "icons": ["assets/icon.png"],
  "platforms": ["macos", "linux"],
  "web_engine": "system",
  "cef": { "dir": "third_party/cef/macos", "auto_install": false },
  "dmg": {
    "background": "assets/dmg-background.png",
    "window_width": 660,
    "window_height": 400,
    "app_position": { "x": 166, "y": 182 },
    "applications_position": { "x": 486, "y": 182 }
  }
}
```

<table>
  <thead>
    <tr>
      <th>
        Field
      </th>

      <th>
        Used for
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `id`
      </td>

      <td>
        macOS bundle identifier, Linux desktop file, log paths
      </td>
    </tr>

    <tr>
      <td>
        `display_name`
      </td>

      <td>
        Menu bar name, window title fallback
      </td>
    </tr>

    <tr>
      <td>
        `version`
      </td>

      <td>
        `Info.plist`

        version, package metadata
      </td>
    </tr>

    <tr>
      <td>
        `icons`
      </td>

      <td>
        One square source image; every platform's icon artifacts generate from it (see App icons)
      </td>
    </tr>

    <tr>
      <td>
        `file_associations`
      </td>

      <td>
        Document type metadata for macOS, Linux, and Windows registration artifacts
      </td>
    </tr>

    <tr>
      <td>
        `url_schemes`
      </td>

      <td>
        Custom protocol metadata for macOS, Linux, and Windows registration artifacts
      </td>
    </tr>

    <tr>
      <td>
        `platforms`
      </td>

      <td>
        Which platform packages to generate
      </td>
    </tr>

    <tr>
      <td>
        `dmg`
      </td>

      <td>
        Optional macOS disk-image branding and Finder layout; defaults already produce a styled drag-to-Applications image
      </td>
    </tr>
  </tbody>
</table>

## App icons

Drop one square image in your project — `assets/icon.png` (1:1, ideally 1024x1024) or `assets/icon.svg` — list it in `icons`, and packaging generates what each platform needs: a complete `.icns` for macOS (with the platform's rounded-rectangle icon shape and margins applied automatically, so full-bleed artwork looks native in the Dock), a multi-size `.ico` for Windows, and hicolor PNG size sets for Linux, plus asset-catalog and launcher-mipmap images for the mobile host skeletons. Artwork that already has transparent corners is treated as pre-shaped and ships unmasked. For art-directed control, a prebuilt `.icns` (macOS) or `.ico` (Windows) in `icons` always wins untouched. Everything is generated by the SDK's own rasterizer and encoders — no external tools. `native validate` checks the source up front (square, decodable, large enough) with the same messages packaging prints.

## macOS

### App bundle

`native package --target macos` creates a `.app` bundle with:

- `Contents/MacOS/<binary>` -- the compiled executable
- `Contents/Resources/AppIcon.icns` -- the app icon, generated from your icon source (or your prebuilt `.icns`, copied untouched under its own name)
- `Contents/Info.plist` -- generated from the app manifest
- `Contents/Resources/assets/` -- the app's asset tree, mirrored at its app-relative path so runtime asset paths resolve inside the bundle
- `Contents/Resources/dist/` -- frontend assets (if configured; replaces the `assets/` mirror)

macOS app bundles declare `LSMinimumSystemVersion` as `11.0`. File associations and URL schemes are emitted as `CFBundleDocumentTypes` and `CFBundleURLTypes` entries in `Contents/Info.plist`.

See [Code Signing](/docs/packaging/signing) for signing, notarization, and DMG creation.

Apps using [native updates](/docs/updates) add `--update-archive` to emit the ZIP consumed by the updater. The updater ZIP contains exactly the packaged `.app`; it is separate from the user-facing DMG.

### DMG archives

Add `--archive` to turn the packaged app into the disk image users download:

```bash
native build
native package --target macos --archive
```

Without any extra configuration, Native creates a 660×400 Finder window with a generated background and arrow, packages matching 1× and 2× representations for crisp Retina rendering, positions the app and an `/Applications` alias on either side, hides the Finder chrome, and compresses the result as a `.dmg`. The package diagnostic prints both the `.app` and `.dmg` paths.

Customize the presentation in `app.json`:

```json title="app.json"
"dmg": {
  "volume_name": "My App",
  "background": "assets/dmg-background.png",
  "window_width": 720,
  "window_height": 440,
  "icon_size": 144,
  "app_position": { "x": 180, "y": 210 },
  "applications_position": { "x": 540, "y": 210 },
  "applications_link": true
}
```

The window dimensions are the usable background canvas, excluding Finder's title bar. Positions are icon centers measured from the canvas's top-left corner. The background must be a project-relative PNG, JPEG, or TIFF at the configured window size; Finder displays it at its natural size. `native validate` and packaging reject malformed images or dimensions that do not match the configured canvas. For Retina artwork, put a double-sized sibling next to it using the `@2x` convention—for example, `dmg-background.png` at 720×440 and `dmg-background@2x.png` at 1440×880. Native discovers the pair, verifies that the sibling is exactly double-sized, and packages both representations. A prebuilt multi-resolution TIFF also works.

Omitting `background` keeps Native's generated Retina-aware gradient and draws the arrow between the configured app and Applications positions. Set `applications_link = false` only for a disk image that is not meant to use the conventional drag-to-install flow.

For complete control over which Finder items appear and where they sit, replace the fixed app/Applications pair with `items`:

```json title="app.json"
"dmg": {
  "background": "assets/dmg-background.png",
  "window_width": 760,
  "window_height": 480,
  "icon_size": 112,
  "items": [
    { "kind": "app", "position": { "x": 150, "y": 180 } },
    { "kind": "applications", "position": { "x": 610, "y": 180 } },
    { "kind": "file", "path": "README.pdf", "name": "Read Me.pdf", "position": { "x": 250, "y": 370 } },
    { "kind": "link", "path": "/Library/QuickLook", "name": "QuickLook", "position": { "x": 510, "y": 370 } }
  ]
}
```

An explicit list must contain exactly one `app`; its optional `name` changes only the bundle name shown in the DMG. `applications` creates the `/Applications` alias. `file` copies a project-relative file or directory, with an optional display `name`, while `link` creates a named symbolic link to an absolute path. When `items` is present, it replaces `app_position`, `applications_position`, and `applications_link`. The generated background draws its arrow whenever the list includes both `app` and `applications`.

## Linux

### Package structure

Linux packaging creates an install tree:

- `bin/<name>` -- the executable
- `share/applications/<name>.desktop` -- desktop entry file
- `share/icons/hicolor/.../<name>.png` -- icons at standard sizes
- `share/mime/packages/<name>.xml` -- shared MIME metadata when file associations are configured

```bash
native package --target linux --manifest app.json --binary zig-out/bin/MyApp
```

Configured file associations and URL schemes are added to the desktop file `MimeType` list. Extension-only associations get generated `application/x-...` MIME types with glob patterns in the shared MIME package.

## Windows

```bash
native package --target windows --manifest app.json --binary zig-out/bin/MyApp.exe
```

Windows packaging is in early development. The packager copies the binary and assets into a distributable directory structure and writes a multi-size `app-icon.ico` generated from your icon source (or copies a prebuilt `.ico` from `.icons` untouched). When file associations or URL schemes are configured, the artifact also includes `install/register-file-types.ps1`, which registers the package-local executable under the current user's `HKCU\Software\Classes` registry keys.

## Frontend assets

### Bundle assets

If your app has a frontend build step, bundle the output:

```bash
zig build bundle-assets
```

This copies the configured `dist` directory into the build output. Production packages serve these through `zero://app/`, so paths like `/assets/app.js` work without `file://` URLs.

### Configure in app.json

```json title="app.json"
"frontend": {
  "dist": "dist",
  "entry": "index.html",
  "spa_fallback": true
}
```

<table>
  <thead>
    <tr>
      <th>
        Field
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `dist`
      </td>

      <td>
        Path to the built frontend output
      </td>
    </tr>

    <tr>
      <td>
        `entry`
      </td>

      <td>
        HTML entry point within

        `dist`
      </td>
    </tr>

    <tr>
      <td>
        `spa_fallback`
      </td>

      <td>
        Serve

        `entry`

        for unknown routes (SPA mode)
      </td>
    </tr>
  </tbody>
</table>

## CEF bundling

When using the Chromium engine on macOS, bundle CEF alongside the app:

```bash
zig build cef-bundle -Dcef-dir=/path/to/cef
```

This copies the required CEF framework, libraries, and resources into the app bundle. The CEF distribution must match the macOS target architecture.

Use the same CEF version for install, build, package, and CI verification. The usual app-developer flow is:

```bash
native cef install --version <pinned-version>
zig build
native package --target macos
```

Set `"web_engine": "chromium"` and `"cef": { "dir": "third_party/cef/macos", "auto_install": false }` in `app.json` for the normal Chromium package path. Use `-Dweb-engine`, `--web-engine`, `-Dcef-dir`, or `--cef-dir` only when you need a one-off override.

Verify the Chromium macOS package layout locally with:

```bash
zig build test-package-cef-layout -Dplatform=macos
```

This gated check requires a local CEF layout or `-Dcef-auto-install=true` and verifies that the packaged app contains the CEF framework and resource files.

## Icon generation

Regenerate the SDK's default app icon from its vector source (`tools/generate_app_icon.zig`):

```bash
zig build generate-icon
```

This renders the full icon family (16 through 1024 px with @2x variants) through the SDK's own path rasterizer, assembles `assets/icon.icns` and `assets/icon.ico` with the built-in container writers (round-trip-validated, no external tools, any host OS), and also emits `assets/icon.png` (the 1024 px master) and an `assets/icon.svg` mirror for design handoff. The default follows the macOS icon grid — a centered 824x824 rounded-rect plate on the 1024 canvas with standard margins — so it sits correctly in the Dock next to system apps. `native init` scaffolds the same design as `assets/icon.png`, the one-image source the packaging pipeline generates every platform's icons from.

## Validation

Check that your manifest and environment are ready for packaging:

```bash
native doctor --manifest app.json --strict
native validate app.json
```

`doctor` checks the host environment, WebView availability, manifest validity, log paths, and optional CEF paths. Add `--strict` to fail on any warning. See [Debugging](/docs/debugging) for details on what `native doctor` checks.

## Platform shortcut commands

In addition to `native package --target <platform>`, the CLI provides shortcut commands:

```bash
native package-windows [--output path] [--binary path] [--service-binary path]
native package-linux [--output path] [--binary path] [--service-binary path]
native package-ios [--output path] [--binary path]
native package-android [--output path] [--binary path]
```

The desktop shortcuts use an explicit `--service-binary` when supplied; otherwise, service-bearing projects discover the normal `zig-out/bin/<app>_services[.exe]` build output just like `native package`.

## Platform targets

<table>
  <thead>
    <tr>
      <th>
        Target
      </th>

      <th>
        Status
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `macos`
      </td>

      <td>
        Full support:

        `.app`

        bundle, signing, notarization, DMG
      </td>
    </tr>

    <tr>
      <td>
        `linux`
      </td>

      <td>
        Desktop entry, icon install, binary packaging
      </td>
    </tr>

    <tr>
      <td>
        `windows`
      </td>

      <td>
        Early support: directory-based packaging
      </td>
    </tr>

    <tr>
      <td>
        `ios`
      </td>

      <td>
        Experimental: complete generated Xcode project around the toolkit UIKit host; ready for

        `xcodebuild archive`

        (code signing stays manual)
      </td>
    </tr>

    <tr>
      <td>
        `android`
      </td>

      <td>
        Experimental: complete generated host project around the toolkit Android host; the debug APK assembles from the generated project directly with the SDK build tools (store signing stays manual)
      </td>
    </tr>
  </tbody>
</table>

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Code Signing
url: "https://native-sdk.dev/docs/packaging/signing"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Sign and notarize your Native SDK app for distribution.

## macOS signing

Sign the bundle with a Developer ID:

```bash
native package --target macos --signing identity --identity "Developer ID Application: Your Name"
```

Identity signing enables the hardened runtime and explicitly requests Apple's secure timestamp. If the timestamp service cannot be reached, packaging fails instead of producing a distribution signature that notarization will reject.

Signing modes:

<table>
  <thead>
    <tr>
      <th>
        Mode
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `none`
      </td>

      <td>
        No signing (default)
      </td>
    </tr>

    <tr>
      <td>
        `adhoc`
      </td>

      <td>
        Ad-hoc signing for local testing
      </td>
    </tr>

    <tr>
      <td>
        `identity`
      </td>

      <td>
        Sign with a named identity (requires

        `--identity`

        )
      </td>
    </tr>
  </tbody>
</table>

## What Gatekeeper shows for an ad-hoc package

An ad-hoc signature verifies bundle integrity but carries no developer identity. Gatekeeper may block a downloaded copy that has the quarantine attribute. The dialog and available override controls depend on the macOS version and device policy; when permitted, users can allow a blocked app in **System Settings → Privacy & Security**.

Local builds and some file-transfer methods do not add quarantine, but an existing quarantine attribute may be preserved. Do not use the absence of a launch dialog as evidence of a distribution signature.

Use `codesign --verify --strict` to check the signature. If verification fails, rebuild and sign the final bundle after copying its resources. For public distribution, use Developer ID signing and notarization.

## Signing flags

<table>
  <thead>
    <tr>
      <th>
        Flag
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `--signing`
      </td>

      <td>
        Signing mode:

        `none`

        ,

        `adhoc`

        , or

        `identity`
      </td>
    </tr>

    <tr>
      <td>
        `--identity`
      </td>

      <td>
        Code signing identity name
      </td>
    </tr>

    <tr>
      <td>
        `--entitlements`
      </td>

      <td>
        Path to entitlements file (e.g.

        `assets/native-sdk.entitlements`

        )
      </td>
    </tr>

    <tr>
      <td>
        `--notarize`
      </td>

      <td>
        Submit the final macOS artifact, then staple and validate the accepted ticket
      </td>
    </tr>

    <tr>
      <td>
        `--notary-profile`
      </td>

      <td>
        Name of a

        `notarytool`

        Keychain profile created with

        `store-credentials`
      </td>
    </tr>
  </tbody>
</table>

## Notarization

Store notarization credentials in Keychain once. A team App Store Connect API key is the normal CI choice:

```bash
xcrun notarytool store-credentials "my-app-release" \
  --key /path/to/AuthKey_KEYID.p8 \
  --key-id KEYID \
  --issuer ISSUER_UUID
```

Apple ID credentials work too:

```bash
xcrun notarytool store-credentials "my-app-release" \
  --apple-id "you@example.com" \
  --team-id TEAMID \
  --password APP_SPECIFIC_PASSWORD
```

Then package, timestamp-sign, notarize, staple, and validate in one command:

```bash
native package --target macos \
  --signing identity \
  --identity "Developer ID Application: Your Name (TEAMID)" \
  --archive \
  --notarize \
  --notary-profile "my-app-release"
```

With `--archive`, Native SDK first submits the signed app, staples and validates it, then builds and signs the DMG and submits, staples, and validates that final image. Without `--archive`, only the app submission runs. Any updater ZIP is created afterward from the stapled app.

The framework repository exposes the same path for local release testing:

```bash
zig build notarize \
  -Didentity="Developer ID Application: Your Name (TEAMID)" \
  -Dnotary-profile="my-app-release"
```

## Chromium apps

Chromium packages include `Chromium Embedded Framework.framework` inside the `.app`. Sign and notarize the final package after CEF has been bundled so the app binary, helper executables, and embedded framework are covered by the same distribution identity.

```bash
native cef install --version <pinned-version>
zig build
native package --target macos --signing identity --identity "Developer ID Application: Your Name" --archive
```

Use `.web_engine = "chromium"` and `.cef = .{ .dir = "third_party/cef/macos", .auto_install = false }` in `app.zon` for the normal signing path. `-Dweb-engine`, `--web-engine`, `-Dcef-dir`, and `--cef-dir` remain available for temporary overrides.

If Gatekeeper rejects the app, check that the CEF framework is present in `Contents/Frameworks`, that every nested helper is signed, and that the package was rebuilt after any CEF version change.

## DMG creation

Create a distributable disk image:

```bash
native package --target macos --archive
```

This creates the conventional drag-to-Applications presentation by default: a generated background and arrow, positioned app and Applications icons, hidden Finder chrome, and a compressed final image. Customize it through the [`dmg` manifest fields](/docs/packaging#dmg-archives).

## Entitlements

The project includes `assets/native-sdk.entitlements` as a starting point. Customize it for your app's needs (e.g. network access, file system access, camera).

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Accordion
url: "https://native-sdk.dev/docs/components/accordion"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `accordion` to show or hide a section of content. `text` supplies the header label, `selected` controls whether the children are visible, and pressing the header dispatches `on-toggle`. Store the open state in the model. The item adjusts its height when opened or closed.

## Markup

```html
<column width="380">
  <accordion text="Is it accessible?" selected="{faq_open}" on-toggle="toggle_faq">
    <column>
      <text wrap="true" foreground="text_muted">Yes. Widgets carry semantic roles and one roving focus set.</text>
    </column>
  </accordion>
  <accordion text="Is it styled?" on-toggle="toggle_styling" />
</column>
```

Store a boolean in the model and toggle it in `update`:

```ts
// model: { readonly faq_open: boolean }
case "toggle_faq":
  return { ...model, faq_open: !model.faq_open };
```

```zig
// model: faq_open: bool = false,
.toggle_faq => model.faq_open = !model.faq_open,
```

A one-open-at-a-time group is a model decision: store the open item's id and compute each item's `selected` from an equality, flipping it in `update`.

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.column(.{ .width = 380 }, .{
    ui.el(.accordion, .{
        .text = "Is it accessible?",
        .selected = model.faq_open,
        .on_toggle = .toggle_faq,
    }, .{
        ui.column(.{}, .{
            ui.text(.{ .wrap = true, .style_tokens = .{ .foreground = .text_muted } }, "Yes. Widgets carry semantic roles and one roving focus set."),
        }),
    }),
    ui.el(.accordion, .{ .text = "Is it styled?", .on_toggle = .toggle_styling }, .{}),
})
```

## Attributes

| Attribute   | Description                                                                                                                          |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `text`      | Text value for text-bearing elements; a literal or one \{binding}.                                                                   |
| `selected`  | Selected state; often a \{a == b} equality.                                                                                          |
| `on-toggle` | Dispatch a Msg on toggle: tag or tag:\{payload}. Hit-target elements only (checkbox, toggle, toggle-button, switch, accordion, ...). |
| `height`    | Definite height (plain number): the element is exactly this tall; content neither shrinks nor overflows it.                          |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Alert
url: "https://native-sdk.dev/docs/components/alert"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `alert` for an inline message. `text` supplies the title, and children supply the description. The title appears beside a 16px icon. Set `variant="destructive"` for a warning icon and destructive colors. The element rejects `gap`; wrap multiple description elements in a `column`.

## Markup

```html
<column gap="12" width="380">
  <alert text="A new version of the shell is available.">
    <text wrap="true" foreground="text_muted">Restart the app to finish updating.</text>
  </alert>
  <alert text="Your session has expired. Sign in again." variant="destructive"></alert>
</column>
```

For a dismissible message, render the alert inside an `if` and clear the visibility flag from a button. Use [dialog](/docs/components/dialog), [drawer](/docs/components/drawer), or [sheet](/docs/components/sheet) for a modal message.

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.column(.{ .gap = 12, .width = 380 }, .{
    ui.el(.alert, .{ .text = "A new version of the shell is available." }, .{
        ui.text(.{ .wrap = true, .style_tokens = .{ .foreground = .text_muted } }, "Restart the app to finish updating."),
    }),
    ui.el(.alert, .{ .text = "Your session has expired. Sign in again.", .variant = .destructive }, .{}),
})
```

## Attributes

| Attribute | Description                                                               |
| --------- | ------------------------------------------------------------------------- |
| `text`    | Text value for text-bearing elements; a literal or one \{binding}.        |
| `variant` | Visual variant: default\|primary\|secondary\|outline\|ghost\|destructive. |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Avatar
url: "https://native-sdk.dev/docs/components/avatar"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `avatar` to display an image with an initials fallback. The element content supplies the initials, and `image` binds to a registered `ImageId`. Register bytes through `fx.registerImageBytes` in a Zig app or declare files in the app manifest’s `assets.images` list. Image ID `0` means no image, so keep that value until registration succeeds.

## Markup

```html
<row gap="12" cross="center">
  <avatar>ZN</avatar>
  <avatar>CT</avatar>
  <avatar image="{profile_image}" label="Profile picture">NS</avatar>
</row>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.avatar(.{}, "ZN")
```

With a registered image the engine clips it to the avatar circle (`cover` fit); the initials remain the fallback while the id is 0:

```zig
ui.avatar(.{ .image = image_id }, "ZN")
```

An avatar can select one region from a registered texture atlas by declaring all four source coordinates in decoded-image pixels. The same crop is `ElementOptions.image_src` in a Zig view:

```html
<avatar image="{atlas_image}" source-x="64" source-y="0"
  source-width="32" source-height="32" label="Profile picture">ZN</avatar>
```

## Attributes

| Attribute       | Description                                                                                                                                                                                                               |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`          | Text value for text-bearing elements; a literal or one \{binding}.                                                                                                                                                        |
| `image`         | avatar and image: one \{binding} to a u64 ImageId the app registered at runtime (Cmd.imageLoad, fx.loadImage, fx.registerImageBytes); 0 draws nothing (an avatar falls back to its initials). Required on the image leaf. |
| `source-x`      | avatar and image: left edge of an optional source crop, in decoded-image pixels. Declare all four source-\* attributes together beside image.                                                                             |
| `source-y`      | avatar and image: top edge of an optional source crop, in decoded-image pixels. Declare all four source-\* attributes together beside image.                                                                              |
| `source-width`  | avatar and image: width of an optional source crop, in decoded-image pixels. Declare all four source-\* attributes together beside image.                                                                                 |
| `source-height` | avatar and image: height of an optional source crop, in decoded-image pixels. Declare all four source-\* attributes together beside image.                                                                                |
| `label`         | Accessible name; when set it REPLACES the element's text as the announced name - screen readers and automation snapshots see the label, never the text it shadows.                                                        |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Badge
url: "https://native-sdk.dev/docs/components/badge"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `badge` to display a count or status. The element content supplies the label and supports interpolation. `variant` selects the colors, and `icon` adds a built-in vector icon before the text. Badges do not handle presses; use [toggle-button](/docs/components/toggle) or [button](/docs/components/button) for an action.

## Markup

```html
<row gap="10" cross="center">
  <badge>Badge</badge>
  <badge variant="secondary">Secondary</badge>
  <badge variant="outline">Outline</badge>
  <badge variant="destructive">Destructive</badge>
  <badge variant="secondary" icon="check">Verified</badge>
</row>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.row(.{ .gap = 10, .cross = .center }, .{
    ui.el(.badge, .{ .text = "Badge" }, .{}),
    ui.el(.badge, .{ .text = "Secondary", .variant = .secondary }, .{}),
    ui.el(.badge, .{ .text = "Outline", .variant = .outline }, .{}),
    ui.el(.badge, .{ .text = "Destructive", .variant = .destructive }, .{}),
    ui.el(.badge, .{ .text = "Verified", .variant = .secondary, .icon = "check" }, .{}),
})
```

## Attributes

| Attribute | Description                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`    | Text value for text-bearing elements; a literal or one \{binding}.                                                                                                                                                                                                                                                                                                                                                                              |
| `variant` | Visual variant: default\|primary\|secondary\|outline\|ghost\|destructive.                                                                                                                                                                                                                                                                                                                                                                       |
| `icon`    | button, toggle-button, list-item, menu-item: vector icon drawn inline (buttons/toggle-buttons before the label, list/menu items as a leading slot): a built-in name (comptime-validated against canvas.icons.known\_icon\_names, e.g. save, plus, refresh-cw), an app-registered app:\<name>, or one \{binding} resolving to such a name. Icon-only buttons when the content is empty — add a label. One hit target, one enabled/disabled tint. |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Breadcrumb
url: "https://native-sdk.dev/docs/components/breadcrumb"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `breadcrumb` to display a navigation hierarchy. Children flow horizontally. Add text elements for ancestors and the current page, with [icons](/docs/components/icon) between them. Set `on-press` on an ancestor’s text element to dispatch a navigation message.

## Markup

```html
<breadcrumb gap="8" cross="center">
  <text foreground="text_muted" on-press="open_home">Home</text>
  <icon name="chevron-right" foreground="text_muted" />
  <text foreground="text_muted" on-press="open_components">Components</text>
  <icon name="chevron-right" foreground="text_muted" />
  <text>Breadcrumb</text>
</breadcrumb>
```

For a model-driven trail, loop the ancestors and dispatch a payload per crumb:

```html
<breadcrumb gap="8" cross="center">
  <for each="crumbs" as="crumb" key="id">
    <text foreground="text_muted" on-press="open_crumb:{crumb.id}">{crumb.title}</text>
    <icon name="chevron-right" foreground="text_muted" />
  </for>
  <text>{current_title}</text>
</breadcrumb>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.el(.breadcrumb, .{ .gap = 8, .cross = .center }, .{
    ui.text(.{ .style_tokens = .{ .foreground = .text_muted }, .on_press = .open_home }, "Home"),
    ui.icon(.{ .style_tokens = .{ .foreground = .text_muted } }, "chevron-right"),
    ui.text(.{ .style_tokens = .{ .foreground = .text_muted }, .on_press = .open_components }, "Components"),
    ui.icon(.{ .style_tokens = .{ .foreground = .text_muted } }, "chevron-right"),
    ui.text(.{}, "Breadcrumb"),
})
```

## Attributes

| Attribute  | Description                                                                                                                                                                                                  |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `gap`      | Spacing between children (plain number). Rejected on stacking containers (stack, panel, card, the modal surfaces) — they layer children; put a column (or row) inside for flow.                              |
| `cross`    | Cross-axis alignment: stretch\|start\|center\|end.                                                                                                                                                           |
| `on-press` | Dispatch a Msg on press: tag or tag:\{payload}. Legal on any element — a bound press handler makes it pressable, and presses on plain text/icons inside it fall through to it (dragging still selects text). |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Bubble
url: "https://native-sdk.dev/docs/components/bubble"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `bubble` for a chat message. Children stack inside the surface. The default variant uses a muted background; `primary` uses the sent-message colors and sets the child text color. `outline`, `destructive`, and `ghost` are also supported. The default padding is 10px vertically and 12px horizontally. The element rejects `gap`; use `padding` to change the inset.

Without an explicit `width`, a bubble fits its content up to 80% of the container width, then wraps. The `ghost` variant uses the full available width. Set `cross="end"` on the containing column to align sent messages to the trailing edge.

## Grouped runs

Group consecutive messages from the same sender in a column. This example uses 8px between messages in a group and 32px between groups:

```html
<column gap="32" width="340">
  <column gap="8">
    <bubble>
      <text wrap="true">Ready to ship the components page?</text>
    </bubble>
    <bubble>
      <text wrap="true">The previews and the docs page both need one more pass before we cut it.</text>
      <reactions>+2</reactions>
    </bubble>
  </column>
  <column gap="8" cross="end">
    <bubble variant="primary">
      <text wrap="true">Previews are rendering now.</text>
    </bubble>
  </column>
</column>
```

## Reactions

Add a `reactions` child for a text badge on the bubble’s bottom edge. Its text supports bindings. `text-alignment` controls placement: `end` is the default, with `start` and `center` also available. Reactions use their own colors, including inside a `primary` bubble.

Reactions do not reserve layout space. Leave enough space below the bubble to avoid overlap with the next message. Each bubble accepts one reaction element; events, keys, and layout properties belong to the bubble. Literal text must be covered by the bundled font. Bind emoji from model data for live platform rendering; the reference renderer cannot render glyphs outside its font.

## Programmatic construction (Zig)

In a Zig view, set `text` and `text_alignment` on the bubble to configure reactions. The builder defaults to `.start`; set `.end` to match the markup default.

```zig
ui.column(.{ .gap = 32, .width = 340 }, .{
    ui.column(.{ .gap = 8 }, .{
        ui.el(.bubble, .{}, .{
            ui.text(.{ .wrap = true }, "Ready to ship the components page?"),
        }),
        ui.el(.bubble, .{ .text = "+2", .text_alignment = .end }, .{
            ui.text(.{ .wrap = true }, "The previews and the docs page both need one more pass before we cut it."),
        }),
    }),
    ui.column(.{ .gap = 8, .cross = .end }, .{
        ui.el(.bubble, .{ .variant = .primary }, .{
            ui.text(.{ .wrap = true }, "Previews are rendering now."),
        }),
    }),
})
```

## Attributes

| Attribute | Description                                                               |
| --------- | ------------------------------------------------------------------------- |
| `padding` | Uniform padding (plain number).                                           |
| `variant` | Visual variant: default\|primary\|secondary\|outline\|ghost\|destructive. |

On the `reactions` child:

| Attribute        | Description                                                                                                                                                                                    |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text-alignment` | reactions: where the pill docks along the bubble's bottom edge — start, center, or end (the default trailing dock). A literal name: the dock is a static design choice, like anchor placement. |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Button Group
url: "https://native-sdk.dev/docs/components/button-group"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `button-group` to attach related [buttons](/docs/components/button). With the default `gap="0"`, adjacent buttons share borders and only the first and last buttons keep their outer corners. Each button retains its own label, `on-press` handler, and disabled state.

For a selected value, use [toggle-group](/docs/components/toggle) or [tabs](/docs/components/tabs). A button group dispatches separate actions.

## Markup

```html
<button-group>
  <button variant="outline" on-press="cut">Cut</button>
  <button variant="outline" on-press="copy">Copy</button>
  <button variant="outline" on-press="paste">Paste</button>
</button-group>
```

## Variants and icon segments

Grouped buttons retain their individual variants. Use `size="icon"` for icon-only actions, such as pagination or zoom controls.

```html
<button-group>
  <button variant="outline" icon="chevron-left" on-press="prev">Back</button>
  <button variant="outline" on-press="today">Today</button>
  <button variant="outline" icon="chevron-right" icon-placement="trailing" on-press="next">Next</button>
</button-group>
```

## Spacing out

`gap` above 0 opts out of the attached treatment: the children render as ordinary separate buttons with full corners and borders. Use a plain [row](/docs/native-ui) if the actions are not related enough to attach.

```html
<button-group gap="8">
  <button variant="outline" on-press="approve">Approve</button>
  <button variant="outline" on-press="reject">Reject</button>
</button-group>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.el(.button_group, .{}, .{
    ui.button(.{ .variant = .outline, .on_press = .cut }, "Cut"),
    ui.button(.{ .variant = .outline, .on_press = .copy }, "Copy"),
    ui.button(.{ .variant = .outline, .on_press = .paste }, "Paste"),
})
```

## Attributes

| Attribute | Description                                                                                                                                                                     |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `gap`     | Spacing between children (plain number). Rejected on stacking containers (stack, panel, card, the modal surfaces) — they layer children; put a column (or row) inside for flow. |
| `cross`   | Cross-axis alignment: stretch\|start\|center\|end.                                                                                                                              |

Children are ordinary buttons; see the [button attributes](/docs/components/button) for the per-segment surface.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Button
url: "https://native-sdk.dev/docs/components/button"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `button` to dispatch a message when pressed. The element content supplies the label, `on-press` names the message, and `icon` adds a built-in vector icon to the same hit target. Set `variant` to choose the color and border treatment.

## Markup

```html
<row gap="12" cross="center">
  <button variant="primary" on-press="save">Save</button>
  <button variant="secondary" on-press="duplicate">Duplicate</button>
  <button variant="outline" on-press="preview">Preview</button>
  <button variant="ghost" on-press="dismiss">Dismiss</button>
  <button variant="destructive" on-press="delete_note">Delete</button>
</row>
```

## Sizes

The supported sizes are `sm`, `default`, `lg`, and `icon`. Use `icon` for a square button and set `label` to provide its accessible name.

```html
<row gap="12" cross="center">
  <button size="sm" variant="outline" on-press="zoom_out">Small</button>
  <button variant="outline" on-press="reset_zoom">Default</button>
  <button size="lg" variant="outline" on-press="zoom_in">Large</button>
  <button size="icon" icon="plus" label="New note" on-press="create_note"></button>
</row>
```

## Icons

`icon` names a [built-in vector icon](/docs/components/icon). The icon shares the button’s hit target and disabled state. For an icon-only button, use `size="icon"`, omit the content, and set `label`.

```html
<row gap="12" cross="center">
  <button variant="primary" icon="download" on-press="download">Download</button>
  <button variant="outline" icon="git-branch" on-press="new_branch">New Branch</button>
  <button disabled="{saving}" on-press="save">Save</button>
</row>
```

## States

Hover and press styling is engine-owned render state; `disabled` is a source attribute (a literal or a model binding).

## Button Group

Use [button-group](/docs/components/button-group) to attach related action buttons with shared borders.

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.row(.{ .gap = 12, .cross = .center }, .{
    ui.button(.{ .variant = .primary, .on_press = .save }, "Save"),
    ui.button(.{ .variant = .secondary, .on_press = .duplicate }, "Duplicate"),
    ui.button(.{ .variant = .outline, .on_press = .preview }, "Preview"),
    ui.button(.{ .variant = .ghost, .on_press = .dismiss }, "Dismiss"),
    ui.button(.{ .variant = .destructive, .on_press = .delete_note }, "Delete"),
})
```

## Attributes

| Attribute   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`      | Text value for text-bearing elements; a literal or one \{binding}.                                                                                                                                                                                                                                                                                                                                                                              |
| `variant`   | Visual variant: default\|primary\|secondary\|outline\|ghost\|destructive.                                                                                                                                                                                                                                                                                                                                                                       |
| `size`      | Control size: default\|sm\|lg\|icon. On text, also the typography rungs heading\|display (themable token steps above title - a section heading, a hero stat); numeric sizes are not accepted - retheme the typography tokens instead.                                                                                                                                                                                                           |
| `icon`      | button, toggle-button, list-item, menu-item: vector icon drawn inline (buttons/toggle-buttons before the label, list/menu items as a leading slot): a built-in name (comptime-validated against canvas.icons.known\_icon\_names, e.g. save, plus, refresh-cw), an app-registered app:\<name>, or one \{binding} resolving to such a name. Icon-only buttons when the content is empty — add a label. One hit target, one enabled/disabled tint. |
| `disabled`  | Disables the control; true/false or a \{binding}.                                                                                                                                                                                                                                                                                                                                                                                               |
| `selected`  | Selected state; often a \{a == b} equality.                                                                                                                                                                                                                                                                                                                                                                                                     |
| `autofocus` | Focusable controls only: moves keyboard focus to the element when it mounts or when the value turns on (edge-triggered - holding it true never re-steals focus). The TEA way to focus an editor on create.                                                                                                                                                                                                                                      |
| `label`     | Accessible name; when set it REPLACES the element's text as the announced name - screen readers and automation snapshots see the label, never the text it shadows.                                                                                                                                                                                                                                                                              |
| `on-press`  | Dispatch a Msg on press: tag or tag:\{payload}. Legal on any element — a bound press handler makes it pressable, and presses on plain text/icons inside it fall through to it (dragging still selects text).                                                                                                                                                                                                                                    |
| `on-hold`   | Press-and-hold Msg: a pointer held \~350 ms dispatches it and the release then presses nothing; a quick click dispatches on-press as usual. A right/ctrl-click with no context menu on the route dispatches it immediately. Like on-press, binding it makes any element pressable.                                                                                                                                                              |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Card
url: "https://native-sdk.dev/docs/components/card"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `card` for a bordered surface with elevation. Children stack on top of each other, so place a `column` or `row` inside for flow layout. The element rejects `gap`. Default padding is 24px, or 16px at `size="sm"`; set `padding` to override it. `on-press` makes the whole card pressable. Use [panel](/docs/components/panel) for a surface without the card treatment.

## Markup

```html
<card width="340">
  <column gap="12">
    <text>Deploy your app</text>
    <text wrap="true" foreground="text_muted">The runtime packages your views, commands, and assets into one native binary.</text>
    <row gap="8">
      <button variant="primary" on-press="deploy">Deploy</button>
      <button variant="ghost" on-press="cancel">Cancel</button>
    </row>
  </column>
</card>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.el(.card, .{ .width = 340 }, .{
    ui.column(.{ .gap = 12 }, .{
        ui.text(.{}, "Deploy your app"),
        ui.text(.{ .wrap = true, .style_tokens = .{ .foreground = .text_muted } }, "The runtime packages your views, commands, and assets into one native binary."),
        ui.row(.{ .gap = 8 }, .{
            ui.button(.{ .variant = .primary, .on_press = .deploy }, "Deploy"),
            ui.button(.{ .variant = .ghost, .on_press = .cancel }, "Cancel"),
        }),
    }),
})
```

## Attributes

| Attribute  | Description                                                                                                                                                                                                  |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `text`     | Text value for text-bearing elements; a literal or one \{binding}.                                                                                                                                           |
| `padding`  | Uniform padding (plain number).                                                                                                                                                                              |
| `width`    | Definite width (plain number): the element is exactly this wide; content neither shrinks nor overflows it. On resizable it is the initial width.                                                             |
| `on-press` | Dispatch a Msg on press: tag or tag:\{payload}. Legal on any element — a bound press handler makes it pressable, and presses on plain text/icons inside it fall through to it (dragging still selects text). |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Chart
url: "https://native-sdk.dev/docs/components/chart"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `chart` to display line, area, or bar series over evenly spaced samples. Colors follow the active theme. The chart reports a series summary to accessibility and automation tools. Data is copied into the view-build arena and downsampled to the per-series point limit using index buckets that preserve minima and maxima. Axis labels and hover details are optional. Charts do not handle presses.

## Markup

A `chart` contains one or more `series` elements. `values` binds to a model iterable of numbers (`f32` in a Zig core): a field, declaration, or function. Axis bounds accept literals or scalar bindings. This example displays two line series with category labels, numeric ticks, and gridlines.

```html
<chart height="180" grid-lines="3" x-labels="{months}" y-labels="true" hover-details="true" label="Request latency">
  <series kind="line" values="{p95_ms}" label="p95" />
  <series kind="line" values="{p50_ms}" color="text_muted" label="p50" />
</chart>
```

The series set is static in markup; values change through bindings. `NaN` samples are not drawn. To align a short history to the right of a fixed window, pad its leading samples with `NaN`.

```ts
/// The 60-sample window, NaN-padded at the front while samples accumulate.
export function cpuSpark(model: Model): readonly number[] {
  const out: number[] = [];
  for (let i = model.cpuHistory.length; i < 60; i++) out.push(NaN);
  for (const sample of model.cpuHistory) out.push(sample);
  return out;
}
```

```zig
/// The 60-sample window, NaN-padded at the front while samples accumulate.
pub fn cpuSpark(model: *const Model, arena: std.mem.Allocator) []const f32 {
    const out = arena.alloc(f32, 60) catch return &.{};
    @memset(out, std.math.nan(f32));
    @memcpy(out[60 - model.cpu_len ..], model.cpuHistory());
    return out;
}
```

Unlabeled charts get a generated series summary (`chart: line cpu 60 pts last 0.42`) as their accessible name, so automation can assert on the data without pixels; `label` replaces it.

## Bar

`kind="bar"` draws one column per sample. A derived y domain includes zero. Set `baseline="true"` to draw the baseline, and provide an x label for each sample. The renderer thins labels when they would overlap.

```html
<chart height="180" baseline="true" x-labels="{weekdays}" hover-details="true" label="Builds per day">
  <series kind="bar" values="{daily_builds}" label="builds" />
</chart>
```

## Area

`kind="area"` fills a line to the baseline. Set `y-min` and `y-max` to keep the scale fixed as values change. This example uses a 0–1 range without category labels; hover details identify samples by index.

```html
<chart height="160" y-min="0" y-max="1" grid-lines="3" hover-details="true" label="Memory">
  <series kind="area" values="{memorySpark}" label="memory" />
</chart>
```

## Axis labels

`x-labels` binds to an iterable of strings, with one label per sample in sample order. Labels render below the plot and are thinned to fit. Downsampling removes category labels because bucket indices no longer correspond to the original samples. `y-labels="true"` adds numeric ticks to the left using steps of 1, 2, or 5 times a power of ten. Gridlines use the same tick positions.

## Hover details

`hover-details="true"` makes the plot hoverable: the pointer snaps to the nearest sample, a hairline cursor marks it (line series get a dot on the hovered point), and a floating card shows the sample's category label with every series' name and value. The card prefers the right side of the cursor and flips left at the window edge. This is interaction-only chrome — static renders never show it — and presses still fall through to whatever is around the chart. Every live preview on this page has it on: hover any of the three.

## Attributes

| Attribute       | Description                                                                                                                                                                                                                                                                             |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `y-min`         | chart: explicit y-domain floor (a number or one \{binding}); omit to derive from the data. Bars always force 0 into a derived domain.                                                                                                                                                   |
| `y-max`         | chart: explicit y-domain ceiling (a number or one \{binding}); omit to derive from the data.                                                                                                                                                                                            |
| `grid-lines`    | chart: horizontal token-hairline gridlines at even divisions (whole number; 0/omitted = none — gridlines are opt-in).                                                                                                                                                                   |
| `baseline`      | chart: draw a hairline at the baseline (zero clamped into the domain); true/false or a \{binding}.                                                                                                                                                                                      |
| `x-labels`      | chart: one \{binding} naming a model iterable of strings — one category label per sample, oldest first (label i names sample i). Drawn muted below the plot, deterministically thinned to fit; dropped when a series downsamples (bucketed indices no longer name the labeled samples). |
| `y-labels`      | chart: numeric y tick labels (true/false or a \{binding}). Ticks ride a nice-step lattice (1/2/5 x 10^k, exact at their precision); with grid-lines set, gridlines share the lattice so grid and labels never disagree.                                                                 |
| `hover-details` | chart: pointer-hover point details (true/false or a \{binding}) — hovering snaps to the nearest sample and floats a card with its label and every series' value. Interaction-only chrome (static renders never show it); presses still fall through.                                    |
| `stroke-width`  | chart: line stroke width override (plain number; default 1.5).                                                                                                                                                                                                                          |
| `width`         | Definite width (plain number); 0/omitted keeps the intrinsic sparkline-sized default (160x48).                                                                                                                                                                                          |
| `height`        | Definite height (plain number); 0/omitted keeps the intrinsic sparkline-sized default (160x48).                                                                                                                                                                                         |
| `grow`          | Flex grow factor.                                                                                                                                                                                                                                                                       |
| `padding`       | Uniform padding (plain number).                                                                                                                                                                                                                                                         |
| `key`           | Sibling-scoped identity key.                                                                                                                                                                                                                                                            |
| `global-key`    | Parent-independent identity: ids survive reparenting between containers.                                                                                                                                                                                                                |
| `label`         | Accessible name; omitted charts get a generated series summary ("chart: line cpu 60 pts last 0.42") so automation reads the data without pixels.                                                                                                                                        |

### Series

`values` is required on every series.

| Attribute | Description                                                                                                                                                                                                   |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `kind`    | series: line, area, or bar (literal; default line). Area is a line filled to the baseline; band envelopes stay with the Zig builder (ui.chart).                                                               |
| `values`  | series: one \{binding} naming a \[]const f32 iterable (a model field, pub decl, or fn - the same sources for each accepts). Required. Pad a short history's leading gap with NaN samples - they draw nothing. |
| `color`   | series: literal color token name (a canvas ColorTokens field, e.g. accent, info, success, text\_muted); default accent. Series retheme with the palette.                                                      |
| `label`   | series: name for the chart's semantics summary ("cpu", "stars"); the kind tag stands in when empty.                                                                                                           |

## Programmatic construction (Zig)

Use the Zig builder for `.band` series with paired lower and upper values, or for a series set constructed at view time. `ui.chart` takes `ChartOptions` and a slice of `canvas.ChartSeries`. Supported kinds are `.line`, `.bar`, and `.band`; `fill` on a line corresponds to markup’s `kind="area"`.

```zig
ui.chart(.{
    .width = 420,
    .height = 180,
    .grid_lines = 3,
    .baseline = true,
    .x_labels = &month_labels,
    .y_labels = true,
    .hover_details = true,
}, &.{
    .{ .kind = .line, .fill = true, .label = "cpu", .values = model.cpu_samples },
    .{ .kind = .bar, .color = .text_muted, .label = "jobs", .values = model.job_counts },
})
```

## Options

`ChartOptions` fields (the builder-side equivalent of the attributes table):

<table>
  <thead>
    <tr>
      <th>
        Option
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `width`
      </td>

      <td>
        Definite plot width (same contract as

        `ElementOptions.width`

        ); 0 keeps the intrinsic sparkline-sized default (160x48).
      </td>
    </tr>

    <tr>
      <td>
        `height`
      </td>

      <td>
        Definite plot height (same contract as

        `ElementOptions.height`

        ); 0 keeps the intrinsic sparkline-sized default.
      </td>
    </tr>

    <tr>
      <td>
        `grow`
      </td>

      <td>
        Flex grow factor; flexes the plot instead of a definite size.
      </td>
    </tr>

    <tr>
      <td>
        `padding`
      </td>

      <td>
        Uniform padding (plain number).
      </td>
    </tr>

    <tr>
      <td>
        `y_min`
      </td>

      <td>
        Explicit y-domain minimum; null derives it from the data (bar series force 0 into a derived domain — bars always have a zero baseline).
      </td>
    </tr>

    <tr>
      <td>
        `y_max`
      </td>

      <td>
        Explicit y-domain maximum; null derives it from the data.
      </td>
    </tr>

    <tr>
      <td>
        `grid_lines`
      </td>

      <td>
        Horizontal token-hairline gridlines (0 = none; gridlines are opt-in, never default). At even divisions of the plot — or on the y-label tick lattice when

        `y_labels`

        is on, so grid and labels share positions.
      </td>
    </tr>

    <tr>
      <td>
        `baseline`
      </td>

      <td>
        Hairline at the baseline (zero clamped into the domain).
      </td>
    </tr>

    <tr>
      <td>
        `x_labels`
      </td>

      <td>
        Category labels for the x axis, one per sample index (empty = no x axis). Muted, in a reserved gutter, thinned to fit; dropped when a series downsamples.
      </td>
    </tr>

    <tr>
      <td>
        `y_labels`
      </td>

      <td>
        Numeric y tick labels on the nice-step lattice, in a measured gutter left of the plot.
      </td>
    </tr>

    <tr>
      <td>
        `hover_details`
      </td>

      <td>
        Pointer-hover point details: snap cursor, per-point dots on line series, and a floating card with the sample's label and every series' value. Interaction-only; presses still fall through.
      </td>
    </tr>

    <tr>
      <td>
        `stroke_width`
      </td>

      <td>
        Line stroke width override (default 1.5).
      </td>
    </tr>

    <tr>
      <td>
        `key`

        /

        `global_key`
      </td>

      <td>
        Widget identity, as on any element.
      </td>
    </tr>

    <tr>
      <td>
        `semantics`
      </td>

      <td>
        Widget semantics; the role defaults to

        `chart`

        , an empty label gets a generated series summary, and the accessibility value reports the first series' latest point.
      </td>
    </tr>
  </tbody>
</table>

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Checkbox
url: "https://native-sdk.dev/docs/components/checkbox"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `checkbox` for a boolean value. The element content or `text` supplies the visible label; `label` supplies an accessible name without drawing text. Bind `checked` to the model and handle `on-toggle` in `update`. Use [radio](/docs/components/radio) to select one option, or [switch](/docs/components/switch) for a sliding on/off control.

## Markup

```html
<column gap="12">
  <checkbox checked="{accepted}" on-toggle="toggle_terms">Accept terms and conditions</checkbox>
  <checkbox checked="{reports}" on-toggle="toggle_reports">Send usage reports</checkbox>
  <checkbox checked="true" disabled="true">Managed by your organization</checkbox>
</column>
```

Store a boolean in the model and toggle it in `update`:

```ts
// model: { readonly accepted: boolean }
case "toggle_terms":
  return { ...model, accepted: !model.accepted };
```

```zig
// model: accepted: bool = false,
.toggle_terms => model.accepted = !model.accepted,
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.column(.{ .gap = 12 }, .{
    ui.checkbox(.{ .text = "Accept terms and conditions", .checked = model.accepted, .on_toggle = .toggle_terms }),
    ui.checkbox(.{ .text = "Send usage reports", .checked = model.reports, .on_toggle = .toggle_reports }),
    ui.checkbox(.{ .text = "Managed by your organization", .checked = true, .disabled = true }),
})
```

## Attributes

| Attribute   | Description                                                                                                                          |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `text`      | Text value for text-bearing elements; a literal or one \{binding}.                                                                   |
| `checked`   | Checked state for checkbox/toggle; true/false or a \{binding}.                                                                       |
| `disabled`  | Disables the control; true/false or a \{binding}.                                                                                    |
| `on-toggle` | Dispatch a Msg on toggle: tag or tag:\{payload}. Hit-target elements only (checkbox, toggle, toggle-button, switch, accordion, ...). |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Code
url: "https://native-sdk.dev/docs/components/code"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `code` to display source text with theme-aware syntax highlighting. It has no background, border, radius, shadow, or padding; add a container to style the surface. Code is selectable and read-only by default. Set `editable` and `on-input` to enable the multiline editor. Code wraps by default; use `wrap="false"` for horizontal scrolling and `line-numbers` to show logical line numbers.

HTML-family highlighting supports HTML, XML, SVG, JSX, and TSX tags, attributes, strings, comments, numbers, and JavaScript or TypeScript expressions.

## Markup

```html
<code
  source="{component_source}"
  language="tsx"
  line-numbers
  wrap="false"
  width="480"
  label="Accordion example"
/>
```

`source` is required and must be one `{binding}` producing text. `language` is a literal lexer name; unknown names are validation errors. Line numbers are off by default and remain decorative, so selecting and copying a numbered block returns only the source text. Numbered presentation is limited to 128 logical lines; longer sources keep all code and omit the gutter.

## Added and removed lines

Diff presentation follows Geist Code Block in the default and Geist theme packs, across light and dark appearances. `added-lines` and `removed-lines` apply full-width green/red washes and renderer-owned `+`/`-` markers while the underlying source, syntax highlighting, selection, and copied text stay unchanged.

```html
<code
  source="{migration_source}"
  language="javascript"
  line-numbers
  added-lines="5"
  removed-lines="2-4"
  wrap="false"
  width="480"
  label="Configuration migration"
/>
```

Line specs are one-based comma lists and inclusive ranges: `added-lines="5, 9-11"`. They annotate clean source—the `+` and `-` are decoration, not bytes callers must splice into the model. This keeps the selected/copied result usable and lets `language` continue highlighting the real grammar. A line cannot be both added and removed. Diff metadata is bounded to lines 1–128; read-only sources longer than 128 lines keep every source byte and omit the diff treatment.

For editable code, apply each `TextInputEvent` to the same model-owned buffer that supplies `source`:

```html
<code
  source="{document}"
  language="tsx"
  editable
  on-input="edit_document"
  line-numbers
  wrap="false"
  grow="1"
  label="Document editor"
/>
```

The editor path includes multiline selection, caret navigation, IME, clipboard, and undo/redo behavior. It does not add textarea background, border, focus ring, or padding.

Surface styling belongs to a wrapper:

```html
<panel padding="12">
  <code source="{component_source}" language="tsx" />
</panel>
```

## Programmatic construction (Zig)

```zig
ui.code(.{
    .language = .javascript,
    .line_numbers = true,
    .added_lines = &.{5},
    .removed_lines = &.{ 2, 3, 4 },
    .wrap = false,
    .width = 480,
    .semantics = .{ .label = "Configuration migration" },
}, model.migration_source)
```

The editable path uses the same `added_lines` and `removed_lines` options when an editor needs annotations; keep those line numbers synchronized as edits change the document.

The Zig builder composes the same way when chrome is wanted:

```zig
ui.panel(.{ .padding = 12 }, .{
    ui.code(.{ .language = .html }, model.component_source),
})
```

The public lexer model is `native_sdk.canvas.code`. `languageFromName` resolves markup spellings, `languageFromFence` reads a Markdown info string, and `highlight` produces the same bounded, theme-colored span runs both renderers use.

## Languages

Zig; JavaScript and TypeScript; JSX and TSX; JSON; YAML; shell; Python; Rust; C, C++, C#, Java, Kotlin, and Swift; Go; HTML, XML, and SVG; CSS, SCSS, and Less; SQL; and Markdown. An omitted language renders plain monospace.

## Attributes

| Attribute       | Description                                                                                                                                                                                               |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `source`        | code: one required \{binding} producing source text (a \[]const u8 field or fn; arena fns work).                                                                                                          |
| `language`      | code: literal lexer name. Supports Zig, JavaScript/TypeScript, JSX/TSX, JSON, YAML, shell, Python, Rust, C-family, Go, HTML/XML/SVG, CSS-family, SQL, and Markdown; unknown names are a validation error. |
| `editable`      | code: true enables text editing while retaining syntax highlighting; pair it with on-input to apply TextInputEvent updates. Read-only by default.                                                         |
| `on-input`      | Names a Msg variant with canvas.TextInputEvent payload; delivers each text edit.                                                                                                                          |
| `line-numbers`  | code: opt into muted logical line numbers. Off by default; a wrapped logical line stays paired with its number.                                                                                           |
| `added-lines`   | code: one-based comma/range spec (for example 5 or 5, 9-11), or one text \{binding}; applies Geist's green full-line wash and renderer-owned + without changing copied source. Lines 1-128.               |
| `removed-lines` | code: one-based comma/range spec (for example 2-4), or one text \{binding}; applies Geist's red full-line wash and renderer-owned - without changing copied source. Lines 1-128.                          |
| `wrap`          | code: true by default. false preserves logical lines and puts the highlighted content in one horizontal scroll region.                                                                                    |
| `width`         | Definite width (plain number).                                                                                                                                                                            |
| `height`        | code: definite height (plain number). Overflow scrolls vertically; with wrap=false the region scrolls on both axes.                                                                                       |
| `min-width`     | Width floor without a definite maximum.                                                                                                                                                                   |
| `grow`          | Flex grow factor.                                                                                                                                                                                         |
| `key`           | Sibling-scoped identity key.                                                                                                                                                                              |
| `global-key`    | Parent-independent identity: ids survive reparenting between containers.                                                                                                                                  |
| `label`         | Accessible name for the code group.                                                                                                                                                                       |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Combobox
url: "https://native-sdk.dev/docs/components/combobox"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `combobox` for a text-entry trigger with a menu affordance. `on-input` dispatches a `TextInputEvent` from `@native-sdk/core/text` in a TypeScript core, or `canvas.TextInputEvent` in a Zig core. Filter options in the model as the user types. Compose the options in an anchored [dropdown-menu](/docs/components/dropdown-menu), as with [select](/docs/components/select). Keep the trigger and menu in a `stack`, render the menu inside an `if`, and clear the open flag in `on-dismiss`.

Enter submits when `on-submit` is bound; otherwise it opens the picker. Space and the Up/Down arrow keys open the picker. Once focus enters the menu, Enter selects the focused `menu-item`.

## Markup

The model owns the query and the open flag; the `for` source is the model-filtered match list, so the menu narrows as the user types.

```html
<stack width="240">
  <combobox placeholder="Search frameworks" text="{frameworkQuery}" on-input="framework_edited" on-submit="commit_framework_query" on-press="open_framework_menu" />
  <if test="{frameworkMenuOpen}">
    <dropdown-menu anchor="below" anchor-alignment="stretch" on-dismiss="close_framework_menu">
      <for each="matchingFrameworks" key="id" as="f">
        <menu-item on-press="pick_framework:{f.id}">{f.name}</menu-item>
      </for>
    </dropdown-menu>
  </if>
</stack>
```

In the primary TypeScript core, the submit arm commits the current model-owned query. The open and submit messages remain separate, so Enter can commit without toggling the picker:

```ts
export type Msg =
  | { readonly kind: "framework_edited"; readonly edit: TextInputEvent }
  | { readonly kind: "open_framework_menu" }
  | { readonly kind: "close_framework_menu" }
  | { readonly kind: "commit_framework_query" };

case "commit_framework_query":
  return { ...model, committedFrameworkQuery: model.frameworkQuery, frameworkMenuOpen: false };
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically. `on_input` takes a comptime message constructor: `Ui.inputMsg(.tag)` builds `Msg{ .tag = edit }` for each `canvas.TextInputEvent`.

```zig
ui.stack(.{ .width = 240 }, .{
    ui.el(.combobox, .{
        .placeholder = "Search frameworks",
        .text = model.framework_query,
        .on_input = Ui.inputMsg(.framework_edited),
        .on_submit = .commit_framework_query,
        .on_press = .open_framework_menu,
    }, .{}),
    if (model.framework_menu_open) ui.el(.dropdown_menu, .{
        .anchor = .below,
        .anchor_alignment = .stretch,
        .on_dismiss = .close_framework_menu,
    }, .{
        ui.el(.menu_item, .{ .text = "Native SDK", .on_press = .{ .pick_framework = 0 } }, .{}),
        ui.el(.menu_item, .{ .text = "Canvas", .on_press = .{ .pick_framework = 1 } }, .{}),
    }) else ui.spacer(0),
})
```

## Attributes

| Attribute     | Description                                                                                                                                                                                                                                                                                                   |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`        | Text value for text-bearing elements; a literal or one \{binding}.                                                                                                                                                                                                                                            |
| `placeholder` | Hint text shown while a text entry is empty.                                                                                                                                                                                                                                                                  |
| `disabled`    | Disables the control; true/false or a \{binding}.                                                                                                                                                                                                                                                             |
| `on-press`    | Dispatch a Msg on press: tag or tag:\{payload}. Legal on any element — a bound press handler makes it pressable, and presses on plain text/icons inside it fall through to it (dragging still selects text).                                                                                                  |
| `on-input`    | Names a Msg variant with canvas.TextInputEvent payload; delivers each text edit.                                                                                                                                                                                                                              |
| `on-submit`   | Dispatch a Msg on submit: tag or tag:\{payload}. Enter in a text field, primary+enter in a textarea; on a list-item, plain Enter dispatches it as the row's primary action while Space keeps the row's select (on-press).                                                                                     |
| `on-dismiss`  | Dismissible surfaces only (dialog, drawer, sheet, dropdown-menu): Msg dispatched when Escape or a click outside dismisses the surface, so the MODEL owns the close (clear the open flag in update). The engine hides the surface immediately as an optimistic echo; the source tree wins on the next rebuild. |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Dialog
url: "https://native-sdk.dev/docs/components/dialog"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `dialog` for a modal surface centered in the root view. `text` supplies the title. Render the dialog inside an `if` bound to an open flag, and clear that flag when `on-dismiss` dispatches after Escape or a click outside. The runtime hides the surface immediately; the source tree determines visibility on the next rebuild.

Children stack over the content box. Start a body column with a fixed-height spacer to leave room for the title. Use [drawer](/docs/components/drawer) or [sheet](/docs/components/sheet) for an edge-aligned surface.

## Markup

```html
<column padding="24">
  <button variant="outline" on-press="open_rename">Rename note</button>
  <if test="{rename_open}">
    <dialog text="Rename note" width="380" height="240" padding="24" on-dismiss="close_rename">
      <column gap="14">
        <spacer height="34"></spacer>
        <text wrap="true" foreground="text_muted">The new name shows up everywhere this note is linked.</text>
        <input text="{draft_name}" label="New name" on-input="name_edited" on-submit="confirm_rename"></input>
        <row gap="8" main="end">
          <button variant="ghost" on-press="close_rename">Cancel</button>
          <button variant="primary" on-press="confirm_rename">Rename</button>
        </row>
      </column>
    </dialog>
  </if>
</column>
```

The open flag is the whole core contract — one field, one arm setting it, one clearing it (`on-dismiss` and the Cancel button share the close arm):

```ts
// model: { readonly rename_open: boolean }
case "open_rename":
  return { ...model, rename_open: true };
case "close_rename":
  return { ...model, rename_open: false };
```

```zig
// model: rename_open: bool = false,
.open_rename => model.rename_open = true,
.close_rename => model.rename_open = false,
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically. The dialog is a plain element kind, so the builder form is `ui.el(.dialog, ...)`; build it only while the model's open flag is set.

```zig
// Rendered only while model.rename_open is true; on_dismiss clears it in update.
ui.el(.dialog, .{ .text = "Rename note", .width = 380, .height = 240, .padding = 24, .on_dismiss = .close_rename }, .{
    ui.column(.{ .gap = 14 }, .{
        ui.column(.{ .height = 34 }, .{}), // clears the engine-drawn title line
        ui.text(.{ .wrap = true, .style_tokens = .{ .foreground = .text_muted } }, "The new name shows up everywhere this note is linked."),
        ui.el(.input, .{ .text = model.draft_name, .on_input = Ui.inputMsg(.name_edited), .on_submit = .confirm_rename, .semantics = .{ .label = "New name" } }, .{}),
        ui.row(.{ .gap = 8, .main = .end }, .{
            ui.button(.{ .variant = .ghost, .on_press = .close_rename }, "Cancel"),
            ui.button(.{ .variant = .primary, .on_press = .confirm_rename }, "Rename"),
        }),
    }),
})
```

## Attributes

Note that `gap` is rejected on the dialog itself — it layers children like the other stacking surfaces; put a column (or row) inside for flow.

| Attribute    | Description                                                                                                                                                                                                                                                                                                   |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`       | Text value for text-bearing elements; a literal or one \{binding}.                                                                                                                                                                                                                                            |
| `width`      | Definite width (plain number): the element is exactly this wide; content neither shrinks nor overflows it. On resizable it is the initial width.                                                                                                                                                              |
| `height`     | Definite height (plain number): the element is exactly this tall; content neither shrinks nor overflows it.                                                                                                                                                                                                   |
| `padding`    | Uniform padding (plain number).                                                                                                                                                                                                                                                                               |
| `on-dismiss` | Dismissible surfaces only (dialog, drawer, sheet, dropdown-menu): Msg dispatched when Escape or a click outside dismisses the surface, so the MODEL owns the close (clear the open flag in update). The engine hides the surface immediately as an optimistic echo; the source tree wins on the next rebuild. |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Drawer
url: "https://native-sdk.dev/docs/components/drawer"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `drawer` for a modal surface pinned to the bottom of the root view. It spans the root width. `text` supplies the title. Render it inside an `if` bound to an open flag, and clear the flag in response to `on-dismiss` after Escape or a click outside. Start a body column with a fixed-height spacer to leave room for the title. See [dialog](/docs/components/dialog) for the shared visibility contract and [sheet](/docs/components/sheet) for a right-edge surface.

## Markup

```html
<column padding="24">
  <button variant="outline" icon="menu" on-press="open_filters">Filters</button>
  <if test="{filters_open}">
    <drawer text="Filters" height="260" padding="24" on-dismiss="close_filters">
      <column gap="12">
        <spacer height="34"></spacer>
        <checkbox checked="{only_unread}" on-toggle="toggle_unread">Only unread</checkbox>
        <checkbox checked="{has_attachments}" on-toggle="toggle_attachments">Has attachments</checkbox>
        <switch checked="{compact_rows}" on-toggle="toggle_compact">Compact rows</switch>
      </column>
    </drawer>
  </if>
</column>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
// Rendered only while model.filters_open is true; on_dismiss clears it in update.
ui.el(.drawer, .{ .text = "Filters", .height = 260, .padding = 24, .on_dismiss = .close_filters }, .{
    ui.column(.{ .gap = 12 }, .{
        ui.column(.{ .height = 34 }, .{}), // clears the engine-drawn title line
        ui.checkbox(.{ .text = "Only unread", .checked = model.only_unread, .on_toggle = .toggle_unread }),
        ui.checkbox(.{ .text = "Has attachments", .checked = model.has_attachments, .on_toggle = .toggle_attachments }),
        ui.el(.switch_control, .{ .text = "Compact rows", .checked = model.compact_rows, .on_toggle = .toggle_compact }, .{}),
    }),
})
```

## Attributes

The drawer rejects `gap` — it layers children like the other stacking surfaces; put a column (or row) inside for flow.

| Attribute    | Description                                                                                                                                                                                                                                                                                                   |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`       | Text value for text-bearing elements; a literal or one \{binding}.                                                                                                                                                                                                                                            |
| `width`      | Definite width (plain number): the element is exactly this wide; content neither shrinks nor overflows it. On resizable it is the initial width.                                                                                                                                                              |
| `height`     | Definite height (plain number): the element is exactly this tall; content neither shrinks nor overflows it.                                                                                                                                                                                                   |
| `padding`    | Uniform padding (plain number).                                                                                                                                                                                                                                                                               |
| `on-dismiss` | Dismissible surfaces only (dialog, drawer, sheet, dropdown-menu): Msg dispatched when Escape or a click outside dismisses the surface, so the MODEL owns the close (clear the open flag in update). The engine hides the surface immediately as an optimistic echo; the source tree wins on the next rebuild. |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Dropdown Menu
url: "https://native-sdk.dev/docs/components/dropdown-menu"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `dropdown-menu` to display a vertical list of `menu-item` elements. `anchor="below"` or `anchor="above"` positions it relative to its parent frame, outside the normal flow. It renders above the tree, is clipped by the window, and flips to the other side if it does not fit. Place the trigger and menu in a `stack`, bind visibility to an open flag, and handle `on-dismiss` after Escape or a click outside.

## Markup

```html
<stack>
  <button variant="outline" icon="chevron-down" on-press="toggle_actions">Actions</button>
  <if test="{actions_open}">
    <dropdown-menu anchor="below" min-width="200" on-dismiss="close_actions">
      <menu-item text="Duplicate" icon="copy" on-press="duplicate_note" />
      <menu-item text="Rename" icon="edit" on-press="rename_note" />
      <menu-item text="Download" icon="download" on-press="download_note" />
      <separator />
      <menu-item text="Delete" icon="trash" on-press="delete_note" />
    </dropdown-menu>
  </if>
</stack>
```

Handle the item's Msg in `update` and clear the open flag there — a menu press does not dismiss implicitly. [Select](/docs/components/select) and [combobox](/docs/components/combobox) build their option lists from exactly this pattern (with `anchor-alignment="stretch"` for the select-menu width).

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.stack(.{}, .{
    ui.button(.{ .variant = .outline, .icon = "chevron-down", .on_press = .toggle_actions }, "Actions"),
    if (model.actions_open) ui.el(.dropdown_menu, .{
        .anchor = .below,
        .min_width = 200,
        .on_dismiss = .close_actions,
    }, .{
        ui.el(.menu_item, .{ .text = "Duplicate", .icon = "copy", .on_press = .duplicate_note }, .{}),
        ui.el(.menu_item, .{ .text = "Rename", .icon = "edit", .on_press = .rename_note }, .{}),
        ui.el(.menu_item, .{ .text = "Download", .icon = "download", .on_press = .download_note }, .{}),
        ui.separator(.{}),
        ui.el(.menu_item, .{ .text = "Delete", .icon = "trash", .on_press = .delete_note }, .{}),
    }) else ui.stack(.{}, .{}),
})
```

## Static menu surface

The builder also has `menu_surface`, an in-flow (non-floating) menu panel with the same `menu_item` children — useful for command palettes and always-visible menus. It is a documented markup exclusion: write it as a Zig view.

```zig
ui.el(.menu_surface, .{ .min_width = 220 }, .{
    ui.el(.menu_item, .{ .text = "Cut", .icon = "edit", .on_press = .cut_selection }, .{}),
    ui.el(.menu_item, .{ .text = "Copy", .icon = "copy", .on_press = .copy_selection }, .{}),
    ui.el(.menu_item, .{ .text = "Paste", .icon = "file-text", .disabled = true }, .{}),
    ui.separator(.{}),
    ui.el(.menu_item, .{ .text = "Select All", .on_press = .select_all }, .{}),
})
```

Right-click context menus are a separate channel with its own element: a `<context-menu>` child on a pressable element (or `ElementOptions.context_menu` in Zig views) presents the platform's native menu — `NSMenu` on macOS, `TrackPopupMenu` on Windows, `GtkPopoverMenu` on Linux — and falls back to an anchored surface at the click point on hosts without one; selections dispatch typed Msgs. The anchored `dropdown-menu` here is for app-designed surfaces you open yourself (pickers, button dropdowns, hold-reveal menus) — see [Menus](/docs/menus).

## Attributes

Dropdown-menu:

| Attribute          | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `anchor`           | dropdown-menu and tooltip: floats the surface against its PARENT's frame instead of the flow (literal below or above; either side auto-flips at the window edges). Late z-pass above the whole tree, window-clipped — never cropped by a scroll pane, never reflows siblings. Put the surface beside its trigger inside a stack. An anchored tooltip's visibility is runtime-owned (hover intent on the trigger; see tooltip-delay), unlike the model-owned dropdown. |
| `anchor-alignment` | dropdown-menu and tooltip (with anchor): horizontal alignment against the anchor - start, end, or stretch (stretch also widens the surface to at least the anchor's width, the select-menu look).                                                                                                                                                                                                                                                                     |
| `anchor-offset`    | dropdown-menu and tooltip (with anchor): literal gap in points between the anchor edge and the surface (default 4).                                                                                                                                                                                                                                                                                                                                                   |
| `on-dismiss`       | Dismissible surfaces only (dialog, drawer, sheet, dropdown-menu): Msg dispatched when Escape or a click outside dismisses the surface, so the MODEL owns the close (clear the open flag in update). The engine hides the surface immediately as an optimistic echo; the source tree wins on the next rebuild.                                                                                                                                                         |
| `min-width`        | Width floor (plain number) without width's definite max: the element may grow past it but never shrink below. On split panes it bounds the divider drag.                                                                                                                                                                                                                                                                                                              |

Menu-item:

| Attribute  | Description                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`     | Text value for text-bearing elements; a literal or one \{binding}.                                                                                                                                                                                                                                                                                                                                                                              |
| `icon`     | button, toggle-button, list-item, menu-item: vector icon drawn inline (buttons/toggle-buttons before the label, list/menu items as a leading slot): a built-in name (comptime-validated against canvas.icons.known\_icon\_names, e.g. save, plus, refresh-cw), an app-registered app:\<name>, or one \{binding} resolving to such a name. Icon-only buttons when the content is empty — add a label. One hit target, one enabled/disabled tint. |
| `disabled` | Disables the control; true/false or a \{binding}.                                                                                                                                                                                                                                                                                                                                                                                               |
| `on-press` | Dispatch a Msg on press: tag or tag:\{payload}. Legal on any element — a bound press handler makes it pressable, and presses on plain text/icons inside it fall through to it (dragging still selects text).                                                                                                                                                                                                                                    |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Icon
url: "https://native-sdk.dev/docs/components/icon"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `icon` to display a vector icon. `name` selects the icon, `foreground` selects a color token, and `width` and `height` set its size. Literal built-in names are checked against the registry at compile time. The same names work in the `icon` attribute on [button](/docs/components/button), toggle-button, list-item, menu-item, and badge.

## Markup

```html
<row gap="20" cross="center">
  <icon name="play"></icon>
  <icon name="search" foreground="text_muted"></icon>
  <icon name="git-branch" width="28" height="28"></icon>
  <icon name="check-circle" foreground="success"></icon>
</row>
```

## App icons and bound names

Names can also come from app registration or model bindings:

- `app:<name>` draws an icon the app registered at boot with `canvas.icons.registerAppIcons` (comptime-parse the SVG with `canvas.svg_icon.parseComptime`). Declare the table as `pub const app_icons` on the app root and pass it to `registerAppIcons` in `main` — the model contract carries the same names, so `native check` verifies every `app:` reference against exactly what the app registers (with a did-you-mean). Without a fresh contract artifact the check degrades to structural checking and says so.
- `icon="{binding}"` defers the choice to model data — a field or function producing a built-in name or an `app:` name per row (a status icon per item, a play/pause toggle). The contract checks the binding resolves to a string.

Both forms resolve at draw time. An unknown name renders a slashed-circle fallback and logs a warning in Debug builds.

```html
<icon name="app:waveform" foreground="text_muted"></icon>
<button icon="{playPauseIcon}" on-press="toggle_play" label="Play or pause"></button>
```

## Registry

The full built-in set (`canvas.icons.known_icon_names`), rendered by the engine.

Built-in icon names: `alert`, `archive`, `arrow-down`, `arrow-right`, `arrow-up`, `check`, `check-circle`, `chevron-down`, `chevron-left`, `chevron-right`, `chevron-up`, `circle-dot`, `clock`, `copy`, `download`, `edit`, `ellipsis`, `external-link`, `eye`, `file-text`, `folder`, `folder-open`, `git-branch`, `git-merge`, `git-pull-request`, `info`, `menu`, `moon`, `music`, `panel-left`, `panel-right`, `pause`, `play`, `plus`, `refresh-cw`, `repeat`, `save`, `search`, `send`, `settings`, `shuffle`, `skip-back`, `skip-forward`, `sun`, `terminal`, `trash`, `volume`, `wrench`, `x`, `x-circle`.

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically. `ui.icon` takes the name as a comptime argument — an unknown name is a compile error pointing at `canvas.icons.known_icon_names`.

```zig
ui.row(.{ .gap = 20, .cross = .center }, .{
    ui.icon(.{}, "play"),
    ui.icon(.{ .style_tokens = .{ .foreground = .text_muted } }, "search"),
    ui.icon(.{ .width = 28, .height = 28 }, "git-branch"),
    ui.icon(.{ .style_tokens = .{ .foreground = .success } }, "check-circle"),
})
```

## Attributes

`name` is the icon element's own required attribute: a comptime-validated built-in name, an `app:<name>` reference to a registered app icon, or one `{binding}` resolving to such a name (not part of the shared attribute vocabulary). The rest are the shared sizing and tint attributes:

| Attribute    | Description                                                                                                                                                        |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `width`      | Definite width (plain number): the element is exactly this wide; content neither shrinks nor overflows it. On resizable it is the initial width.                   |
| `height`     | Definite height (plain number): the element is exactly this tall; content neither shrinks nor overflows it.                                                        |
| `foreground` | Foreground/text color token (literal ColorTokens field name, e.g. text, text\_muted, success, warning, info).                                                      |
| `label`      | Accessible name; when set it REPLACES the element's text as the announced name - screen readers and automation snapshots see the label, never the text it shadows. |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Components
url: "https://native-sdk.dev/docs/components"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Browse the built-in Native components. Each page includes markup, attributes, a rendered preview, and Zig builder examples. Where an example dispatches a message, TypeScript core code appears first with a Zig alternative.

For layout, bindings, and the runtime contract, see [Native UI](/docs/native-ui).

- [Accordion](/docs/components/accordion) — Disclosure surface with a model-owned open state.
- [Alert](/docs/components/alert) — Inline callouts with icon and variant color.
- [Avatar](/docs/components/avatar) — Initials fallback and runtime-registered images.
- [Badge](/docs/components/badge) — Status labels in every variant.
- [Breadcrumb](/docs/components/breadcrumb) — Hierarchy trail with separators.
- [Bubble](/docs/components/bubble) — Chat-message surfaces for either side.
- [Button](/docs/components/button) — Variants, sizes, inline icons, and states.
- [Button Group](/docs/components/button-group) — Attached action buttons as one segmented bar.
- [Card](/docs/components/card) — The bordered, elevated surface container.
- [Chart](/docs/components/chart) — Line, area, and bar charts; band series in the Zig builder.
- [Checkbox](/docs/components/checkbox) — Binary choice with model-owned state.
- [Code](/docs/components/code) — Highlighted source with line numbers and optional horizontal scrolling.
- [Combobox](/docs/components/combobox) — Text entry with an anchored suggestions menu.
- [Dialog](/docs/components/dialog) — Modal surface with model-owned dismissal.
- [Drawer](/docs/components/drawer) — Bottom-anchored modal surface.
- [Dropdown Menu](/docs/components/dropdown-menu) — Anchored floating menus and menu items.
- [Icon](/docs/components/icon) — The built-in vector icon registry.
- [Input](/docs/components/input) — Single-line text entry: input, text field, search field.
- [Input Group](/docs/components/input-group) — One bordered field: textarea plus accessory actions.
- [List](/docs/components/list) — Rows with icons, selection, and virtualization.
- [Markdown](/docs/components/markdown) — GFM rendering through native widgets.
- [Media Surface](/docs/components/media-surface) — Externally produced textures: video, camera, mpv.
- [Pagination](/docs/components/pagination) — Page navigation row.
- [Panel](/docs/components/panel) — The plain surface container.
- [Progress](/docs/components/progress) — Determinate progress bar.
- [Radio](/docs/components/radio) — Single choice within a radio group.
- [Resizable](/docs/components/resizable) — Panel with an engine-managed drag handle.
- [Scroll](/docs/components/scroll) — Scroll regions with model-observable offsets.
- [Select](/docs/components/select) — Trigger plus the anchored dropdown options pattern.
- [Segmented Control](/docs/components/segmented-control) — Standalone selected segments with typed press events.
- [Separator](/docs/components/separator) — Hairline rules, horizontal and vertical.
- [Sheet](/docs/components/sheet) — Right-anchored modal surface.
- [Skeleton](/docs/components/skeleton) — Loading placeholders that sketch the content.
- [Slider](/docs/components/slider) — Continuous value control.
- [Spacer](/docs/components/spacer) — Flexible empty space between siblings.
- [Spinner](/docs/components/spinner) — Indeterminate progress leaf.
- [Split](/docs/components/split) — Draggable two-pane splitter.
- [Status Bar](/docs/components/status-bar) — Window-bottom status text.
- [Stepper](/docs/components/stepper) — Stage progress with completed/active/pending steps.
- [Switch](/docs/components/switch) — On/off switches with model-owned state.
- [Table](/docs/components/table) — Rows and cells with hairline dividers.
- [Tabs](/docs/components/tabs) — Tab strip over segmented controls.
- [Textarea](/docs/components/textarea) — Multi-line text entry.
- [Timeline](/docs/components/timeline) — Ledger list with indicators and connectors.
- [Toggle](/docs/components/toggle) — Pressed-state toggles, toggle buttons, and groups.
- [Tooltip](/docs/components/tooltip) — The floating label above the control it annotates.
- [Tree](/docs/components/tree) — Disclosure tree with one roving focus set.
- [Video](/docs/components/video) — Platform-decoded playback with optional transport controls.
- [Virtual List](/docs/components/virtual-list) — Windowed rows: the view builds only what's visible.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Input Group
url: "https://native-sdk.dev/docs/components/input-group"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `input-group` to combine a [textarea](/docs/components/textarea) and an accessory row inside one border. The group shows a focus ring when any child has focus and removes the textarea’s individual surface styling. The textarea retains its text bindings and input events. Set `submit-on-enter="true"` to submit on Enter, and `autofocus` to focus it when mounted.

## Markup

The textarea comes first (document order is focus order), then the optional `input-group-actions` row. Put a `spacer` between the leading and trailing controls; `if`/`else` inside the row swap send for stop while a run is streaming.

```html
<input-group label="Message composer" height="120">
  <textarea placeholder="Type a message" text="{draft}" submit-on-enter="true" on-input="draft_edited" on-submit="send" />
  <input-group-actions>
    <button icon="plus" variant="ghost" size="icon" on-press="attach" label="Attach"></button>
    <spacer grow="1" />
    <button icon="send" size="icon" on-press="send" label="Send"></button>
  </input-group-actions>
</input-group>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.inputGroup(.{
    .height = 120,
    .semantics = .{ .label = "Message composer" },
}, ui.el(.textarea, .{
    .placeholder = "Type a message",
    .text = model.draft,
    .submit_on_enter = true,
    .on_input = Ui.inputMsg(.draft_edited),
    .on_submit = .send,
    .semantics = .{ .label = "Message" },
}, .{}), ui.inputGroupActions(.{}, .{
    ui.el(.button, .{ .icon = "plus", .variant = .ghost, .size = .icon, .on_press = .attach, .semantics = .{ .label = "Attach" } }, .{}),
    ui.spacer(1),
    ui.el(.button, .{ .icon = "send", .size = .icon, .on_press = .send, .semantics = .{ .label = "Send" } }, .{}),
}))
```

## Attributes

| Attribute   | Description                                                                                                                              |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `label`     | Accessible name; the group announces as ONE named field (role group) while the entry and accessory controls stay individually reachable. |
| `width`     | Definite group width (plain number); 0/omitted sizes from the entry plus the actions row.                                                |
| `height`    | Definite group height (plain number); the entry grow-stretches to absorb it.                                                             |
| `min-width` | Width floor (plain number) without width's definite max.                                                                                 |
| `grow`      | Flex grow factor.                                                                                                                        |

### input-group-actions

| Attribute | Description                                                                            |
| --------- | -------------------------------------------------------------------------------------- |
| `gap`     | input-group-actions: spacing between the accessory controls (plain number; default 6). |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Input
url: "https://native-sdk.dev/docs/components/input"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `input` or `text-field` for single-line text entry. Both support model-bound `text` and `placeholder`, `on-input` for edit events, and `on-submit` for Enter. Edit payloads use `TextInputEvent` from `@native-sdk/core/text` in a TypeScript core or `canvas.TextInputEvent` in a Zig core. Use `search-field` for the same editing behavior with search styling.

## Markup

```html
<column gap="12" width="280">
  <input placeholder="Email address" text="{email}" on-input="email_edited" on-submit="subscribe" />
  <text-field placeholder="Project name" text="{project_name}" on-input="name_edited" />
  <input placeholder="Disabled" disabled="true" />
</column>
```

Keep the text in the model and apply each edit in `update`. See [Messages](/docs/native-ui#messages) for the reducer contract:

```ts
import { type TextInputEvent } from "@native-sdk/core/text";
// the same module ships the full byte-splice reducer (applyTextInputEvent)
// for update to fold each edit over the model's draft bytes

export type Msg =
  | { readonly kind: "subscribe" }
  | { readonly kind: "email_edited"; readonly edit: TextInputEvent };
```

```zig
email_buffer: canvas.TextBuffer(128) = .{},
pub fn email(model: *const Model) []const u8 {
    return model.email_buffer.text();
}
// in update:
.email_edited => |edit| model.email_buffer.apply(edit),
```

## Editing keys

The built-in editor follows the platform keymap in `input`, `text-field`, `search-field`, and `combobox`: Backspace/Delete remove one caret unit, Option+Backspace/Delete on macOS (Ctrl+Backspace/Delete elsewhere) remove one word, and Command+Backspace on macOS (with or without Shift) deletes from the caret to the beginning of the field. A non-empty selection always wins and is deleted by itself. The semantic edit still arrives through `on-input`, so `applyTextInputEvent` and `TextBuffer` keep controlled fields synchronized and one undo restores the whole deletion.

## Search field

`search-field` uses the same bindings as `input`. Pair it with a model-filtered list. When it contains text, it displays a clear button. Pressing that button or Escape while focused dispatches a clear edit through `on-input`. For a suggestions menu, see [combobox](/docs/components/combobox).

```html
<search-field width="280" placeholder="Search notes" text="{query}" on-input="query_edited" />
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically. `on_input` takes a comptime message constructor: `Ui.inputMsg(.tag)` builds `Msg{ .tag = edit }` for each `canvas.TextInputEvent`.

```zig
ui.column(.{ .gap = 12, .width = 280 }, .{
    ui.el(.input, .{
        .placeholder = "Email address",
        .text = model.email,
        .on_input = Ui.inputMsg(.email_edited),
        .on_submit = .subscribe,
    }, .{}),
    ui.textField(.{ .placeholder = "Project name", .text = model.project_name, .on_input = Ui.inputMsg(.name_edited) }),
    ui.el(.input, .{ .placeholder = "Disabled", .disabled = true }, .{}),
})
```

The search field builds the same way:

```zig
ui.el(.search_field, .{
    .width = 280,
    .placeholder = "Search notes",
    .text = model.query,
    .on_input = Ui.inputMsg(.query_edited),
}, .{})
```

## Attributes

| Attribute     | Description                                                                                                                                                                                                                  |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`        | Text value for text-bearing elements; a literal or one \{binding}.                                                                                                                                                           |
| `placeholder` | Hint text shown while a text entry is empty.                                                                                                                                                                                 |
| `value`       | Value for slider/progress/text entry; a literal or one \{binding}.                                                                                                                                                           |
| `disabled`    | Disables the control; true/false or a \{binding}.                                                                                                                                                                            |
| `autofocus`   | Focusable controls only: moves keyboard focus to the element when it mounts or when the value turns on (edge-triggered - holding it true never re-steals focus). The TEA way to focus an editor on create.                   |
| `on-input`    | Names a Msg variant with canvas.TextInputEvent payload; delivers each text edit.                                                                                                                                             |
| `on-submit`   | Dispatch a Msg on submit: tag or tag:\{payload}. Enter in a text field, primary+enter in a textarea; on a list-item, plain Enter dispatches it as the row's primary action while Space keeps the row's select (on-press).    |
| `on-change`   | Dispatch a Msg on change: tag or tag:\{payload}. Hit-target elements only. On a treeitem, Up/Down/Left/Right/Home/End focus movement prefers on-change over on-press, separating keyboard selection from pointer activation. |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: List
url: "https://native-sdk.dev/docs/components/list"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `list` to arrange `list-item` rows vertically. Each row accepts a text label, leading `icon`, model-bound `selected` state, `disabled`, and an `on-press` message. For a long list with fixed-height rows, enable `virtualized` and set `virtual-item-extent` to limit layout and painting to visible rows.

## Markup

```html
<list width="340" gap="2">
  <list-item icon="file-text" selected="{doc == 'report'}" on-press="open_report">Quarterly report.md</list-item>
  <list-item icon="file-text" selected="{doc == 'checklist'}" on-press="open_checklist">Launch checklist.md</list-item>
  <list-item icon="folder" on-press="open_archive">Archive</list-item>
  <list-item icon="music" disabled="true">demo-track.wav</list-item>
</list>
```

## Selection and activation

A single click dispatches `on-press`. To separate selection from activation, handle selection there and bind an action such as opening a record to `on-double-press` and `on-submit`:

```html
<list-item
  selected="{selected_id == track.id}"
  on-press="select_track:{track.id}"
  on-double-press="play_track:{track.id}"
  on-submit="play_track:{track.id}"
>{track.title}</list-item>
```

The Zig builder spells the same channels with underscores:

```zig
ui.listItem(.{
    .selected = model.selected == track.id,
    .on_press = Msg{ .select_track = track.id },      // click, and Space on a ring-focused row
    .on_double_press = Msg{ .play_track = track.id }, // double click: the primary action
    .on_submit = Msg{ .play_track = track.id },       // plain Enter: the same primary action
}, track.title)
```

The first click dispatches selection immediately; the second click dispatches the double-press action. With `on-submit` bound, Enter activates the row and Space selects it. Without it, Enter selects.

The arrows stay app-owned after clicking around: pointer focus on a plain list row is quiet (no ring), and a quietly focused row routes no keys — arrows and Enter fall through to the app-level key fallback, where a selection-owning app moves its own selection. Tab onto a row draws the ring and restores the row's full keymap (arrows walk the rows, Space selects, Enter submits), and rows carrying `role="treeitem"` keep the [tree](/docs/components/tree)'s roving keymap under either register. The full routing order is in [Native UI § Keyboard routing](/docs/native-ui#keyboard-routing-focus-registers-quiet-list-rows-and-the-app-level-fallback); `examples/soundboard`'s track lists are the live reference for the whole pattern.

## Virtualization

Enable `virtualized` and set `virtual-item-extent` for fixed-height rows. Use a keyed `for` loop to build rows from model data. All rows are still built; virtualization limits layout and painting. To build only the visible window, use the Zig builder’s [virtual list](/docs/components/virtual-list).

```html
<list virtualized="true" virtual-item-extent="28" grow="1">
  <for each="rows" as="row" key="id">
    <list-item on-press="open_row:{row.id}">{row.title}</list-item>
  </for>
</list>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.list(.{ .width = 340, .gap = 2 }, .{
    ui.listItem(.{ .icon = "file-text", .selected = model.doc == .report, .on_press = .open_report }, "Quarterly report.md"),
    ui.listItem(.{ .icon = "file-text", .selected = model.doc == .checklist, .on_press = .open_checklist }, "Launch checklist.md"),
    ui.listItem(.{ .icon = "folder", .on_press = .open_archive }, "Archive"),
    ui.listItem(.{ .icon = "music", .disabled = true }, "demo-track.wav"),
})
```

## Attributes

List:

| Attribute             | Description                                                                                                                                                                     |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `virtualized`         | Enable list virtualization (true/false).                                                                                                                                        |
| `virtual-item-extent` | Fixed item extent for virtualized lists (plain number).                                                                                                                         |
| `gap`                 | Spacing between children (plain number). Rejected on stacking containers (stack, panel, card, the modal surfaces) — they layer children; put a column (or row) inside for flow. |

List-item:

| Attribute         | Description                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`            | Text value for text-bearing elements; a literal or one \{binding}.                                                                                                                                                                                                                                                                                                                                                                              |
| `icon`            | button, toggle-button, list-item, menu-item: vector icon drawn inline (buttons/toggle-buttons before the label, list/menu items as a leading slot): a built-in name (comptime-validated against canvas.icons.known\_icon\_names, e.g. save, plus, refresh-cw), an app-registered app:\<name>, or one \{binding} resolving to such a name. Icon-only buttons when the content is empty — add a label. One hit target, one enabled/disabled tint. |
| `selected`        | Selected state; often a \{a == b} equality.                                                                                                                                                                                                                                                                                                                                                                                                     |
| `disabled`        | Disables the control; true/false or a \{binding}.                                                                                                                                                                                                                                                                                                                                                                                               |
| `on-press`        | Dispatch a Msg on press: tag or tag:\{payload}. Legal on any element — a bound press handler makes it pressable, and presses on plain text/icons inside it fall through to it (dragging still selects text).                                                                                                                                                                                                                                    |
| `on-double-press` | Dispatch a Msg on the second release of a double click: tag or tag:\{payload}. The first release still dispatches on-press; the second dispatches this in place of another press. Legal on any element and makes it pressable, like on-press.                                                                                                                                                                                                   |
| `on-submit`       | Dispatch a Msg on submit: tag or tag:\{payload}. Enter in a text field, primary+enter in a textarea; on a list-item, plain Enter dispatches it as the row's primary action while Space keeps the row's select (on-press).                                                                                                                                                                                                                       |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Markdown
url: "https://native-sdk.dev/docs/components/markdown"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `markdown` to render a string as selectable native content. It supports a GFM subset, including pipe tables and a limited set of HTML elements. `source` is required, accepts one model binding, and the element takes no children.

Pass loaded images through `images` and handle links through `on-link`. Bare URLs are linked automatically. `on-details` and model-owned `details-expanded` flags control disclosure blocks; `issue-link-base` turns references such as `#123` into links. Fenced blocks use the [code](/docs/components/code) component.

## Markup

```html
<markdown source="{release_notes}" images="{markdown_images}" on-link="open_link" issue-link-base="https://github.com/vercel-labs/native/issues/"></markdown>
```

## Programmatic construction (Zig)

The builder is the `canvas.markdown` module, parameterized over the app's Msg type; `on_link` pairs with `Ui.linkMsg(.tag)`.

```zig
const Md = native_sdk.canvas.markdown.Markdown(Msg);

Md.view(ui, model.release_notes, .{
    .images = model.markdown_images,
    .on_link = Ui.linkMsg(.open_link),
    .issue_link_base = "https://github.com/vercel-labs/native/issues/",
})
```

Image discovery writes canonical, entity-decoded URLs into caller-owned bounded storage. Consume each `value()` while that storage is alive, copy accepted sources into the model, and use the same bytes for both `fx.loadImage` and the eventual `ResolvedImage.source` mapping:

```zig
var source_storage: [canvas.markdown.max_markdown_images]canvas.markdown.CollectedImageSource = undefined;
const sources = canvas.markdown.collectImageSources(model.release_notes, &source_storage);
for (sources) |*collected| {
    const source = collected.value();
    // Validate the scheme, copy source into the model, then issue fx.loadImage.
}
```

## HTML subset

README- and comment-style HTML is lowered onto native presentation rather than passed to a browser:

- Text: `<b>`, `<strong>`, `<i>`, `<em>`, `<var>`, `<s>`, `<strike>`, `<del>`, `<u>`, `<ins>`, `<code>`, `<kbd>`, `<samp>`, `<tt>`, `<mark>`, `<small>`, `<sub>`, and `<sup>`.
- Content: `<a href>`, `<img src alt width height>`, `<br>`, `<wbr>`, `<q>`, HTML comments, core named entities, and numeric entities. A leading image in a paragraph, heading, list item, blockquote, or pipe-table cell becomes a native image when `images` contains a successful `canvas.markdown.ResolvedImage` mapping for its source; unresolved and mid-paragraph images fall back to alt text. The view never fetches remote media — discover bounded sources with `canvas.markdown.collectImageSources`, read each caller-owned `CollectedImageSource` through `value()`, load it through `fx.loadImage`, retain successful ids and dimensions in the model, and pass mappings with that canonical source back on the next rebuild.
- Blocks and wrappers: `<h1>` through `<h6>`, `<p>`, `<blockquote>`, `<pre>`, `<hr>`, list items, and the common `<div align="center">` README pattern. Harmless list, table, and container wrappers are accepted as readable native content; use Markdown pipe tables for full native table layout. `<details>` and `<summary>` retain their controlled expansion behavior.

This is a sanitized native subset, not a DOM. CSS, scripts, forms, embeds, event attributes, and arbitrary HTML do not execute; unsupported or malformed tags remain literal text. Only `href`, image `src`/`alt`/dimensions, and block `align` affect presentation.

## Attributes

| Attribute          | Description                                                                                                                                                                                                                                                           |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `source`           | markdown: one \{binding} producing the markdown text (a \[]const u8 field or fn; arena fns work). Required.                                                                                                                                                           |
| `images`           | markdown: one \{binding} producing \[]const canvas.markdown.ResolvedImage (arena fns work). Each mapping pairs a leading image source with an image id already loaded and registered through fx.loadImage plus its decoded dimensions; views never perform image I/O. |
| `on-link`          | markdown: bare Msg tag dispatched on link press; its payload is the URL (\[]const u8 variant).                                                                                                                                                                        |
| `on-details`       | markdown: bare Msg tag dispatched on a \<details> summary press; its payload is the block index (usize variant).                                                                                                                                                      |
| `details-expanded` | markdown: \{binding} naming a \[]const bool iterable of expanded flags, in details-block document order.                                                                                                                                                              |
| `issue-link-base`  | markdown: literal URL prefix or one \{binding}; '#123' refs become links to base ++ number (ghissue:// or https\://github.com/owner/repo/issues/).                                                                                                                    |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Media Surface
url: "https://native-sdk.dev/docs/components/media-surface"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `media-surface` to display a texture from a video decoder, camera, or external renderer. It participates in layout, clipping, transforms, and rounded corners like other widgets.

`surface` binds to a model-owned surface id (`u64` in Zig). A Zig toolkit extension acquires a producer for the same id through `runtime.acquireMediaSurfaceProducer` and submits tightly packed RGBA8 frames. The compositor uses the newest frame, paces producers against presented frames, and tracks damage.

Use a nonzero id below bit 63; `0` means unbound, and the high bit is reserved for toolkit textures. If the id comes from a hash, clear its top bit.

Live hosts display an id-derived placeholder until the first frame arrives. The reference renderer always displays that placeholder. Reference screenshots, goldens, fingerprints, and replay verification exclude producer pixels, so a recorded session can replay without an attached producer.

## Markup

```html
<column gap="10">
  <media-surface surface="{preview}" height="180" radius="md" label="Camera preview" />
  <text foreground="text_muted">Awaiting producer frames</text>
</column>
```

The surface has no intrinsic size — like `image`, give it definite `width`/`height` or flex `grow`. It is display-only: presses fall through to the nearest pressable ancestor, and the a11y lint asks for a `label` describing the content.

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same leaf; the surface id rides the `image` channel (the same model-owned u64 shape registered images use):

```zig
ui.mediaSurface(.{ .image = model.preview_surface, .height = 180, .semantics = .{ .label = "Camera preview" } })
```

## Producing frames

Producers are Zig-tier toolkit extensions in this release — acquire a handle for the surface id and push frames from your decoder or render loop:

```zig
const producer = try runtime.acquireMediaSurfaceProducer(model.preview_surface);
defer producer.release();
// From any thread, e.g. the decoder's:
try producer.pushFrame(width, height, rgba8);
```

See [Media Producers](/docs/media-producers) for the full recipe: thread contract, pacing, teardown, and the external-renderer (mpv) shape.

## Attributes

| Attribute    | Description                                                                                                                                                                                                                                                                                                                                                                           |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `surface`    | media-surface: one \{binding} to the model-owned u64 surface id a Zig-tier producer targets (runtime.acquireMediaSurfaceProducer). Required; surface ids are model data, never markup literals; 0 leaves the surface unbound and it draws nothing, and usable ids are nonzero values below bit 63 — the reserved media-surface texture namespace, which the producer acquire refuses. |
| `width`      | Definite width (plain number): the element is exactly this wide; content neither shrinks nor overflows it. On resizable it is the initial width.                                                                                                                                                                                                                                      |
| `height`     | Definite height (plain number): the element is exactly this tall; content neither shrinks nor overflows it.                                                                                                                                                                                                                                                                           |
| `grow`       | Flex grow factor; give spacer one.                                                                                                                                                                                                                                                                                                                                                    |
| `radius`     | Corner radius token (literal RadiusTokens field name: sm, md, lg, xl, none).                                                                                                                                                                                                                                                                                                          |
| `label`      | Accessible name; when set it REPLACES the element's text as the announced name - screen readers and automation snapshots see the label, never the text it shadows.                                                                                                                                                                                                                    |
| `key`        | Sibling-scoped identity key; on for, names an item field.                                                                                                                                                                                                                                                                                                                             |
| `global-key` | Parent-independent identity: ids survive reparenting between containers.                                                                                                                                                                                                                                                                                                              |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Pagination
url: "https://native-sdk.dev/docs/components/pagination"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `pagination` to arrange page-navigation buttons horizontally. The model owns the current page, and each button dispatches an `on-press` message. Use `ghost` for page numbers and `outline` with `selected` for the current page. Previous and next buttons can use `chevron-left` and `chevron-right`; an `ellipsis` icon indicates omitted pages.

## Markup

```html
<pagination>
  <button variant="ghost" icon="chevron-left" on-press="prev_page">Previous</button>
  <for each="pages" as="p" key="number">
    <button variant="{p.variant}" selected="{p.number == page}" on-press="open_page:{p.number}">{p.label}</button>
  </for>
  <icon name="ellipsis" />
  <button variant="ghost" icon="chevron-right" icon-placement="trailing" on-press="next_page">Next</button>
</pagination>
```

Use `outline` to distinguish the current page visually and `selected` to expose its state to accessibility tools. In a `for` loop, a row field or function can return `outline` or `ghost` for the variant.

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.el(.pagination, .{}, .{
    ui.button(.{ .variant = .ghost, .icon = "chevron-left", .on_press = .prev_page }, "Previous"),
    ui.button(.{ .variant = .outline, .selected = model.page == 1, .on_press = .{ .open_page = 1 } }, "1"),
    ui.button(.{ .variant = .ghost, .on_press = .{ .open_page = 2 } }, "2"),
    ui.button(.{ .variant = .ghost, .on_press = .{ .open_page = 3 } }, "3"),
    ui.icon(.{}, "ellipsis"),
    ui.button(.{ .variant = .ghost, .icon = "chevron-right", .icon_placement = .trailing, .on_press = .next_page }, "Next"),
})
```

## Attributes

| Attribute        | Description                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `selected`       | Selected state; often a \{a == b} equality.                                                                                                                                                                                                                                                                                                                                                                                                     |
| `on-press`       | Dispatch a Msg on press: tag or tag:\{payload}. Legal on any element — a bound press handler makes it pressable, and presses on plain text/icons inside it fall through to it (dragging still selects text).                                                                                                                                                                                                                                    |
| `icon`           | button, toggle-button, list-item, menu-item: vector icon drawn inline (buttons/toggle-buttons before the label, list/menu items as a leading slot): a built-in name (comptime-validated against canvas.icons.known\_icon\_names, e.g. save, plus, refresh-cw), an app-registered app:\<name>, or one \{binding} resolving to such a name. Icon-only buttons when the content is empty — add a label. One hit target, one enabled/disabled tint. |
| `icon-placement` | Icon slot side on label-bearing buttons/toggle-buttons: leading (default) draws the icon before the label, trailing after it — the next-page chevron. Icon-only buttons center the glyph regardless.                                                                                                                                                                                                                                            |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Panel
url: "https://native-sdk.dev/docs/components/panel"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `panel` for a surface with a background, border, and radius. Its children stack over one another; `gap` is rejected. Put a row or column inside to arrange content. Bind `on-press` to make the surface pressable. See [Native UI](/docs/native-ui) for color tokens.

## Markup

```html
<panel width="340" padding="16">
  <column gap="6">
    <text>Build output</text>
    <text wrap="true" foreground="text_muted">Build completed.</text>
  </column>
</panel>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.panel(.{ .width = 340, .padding = 16 }, .{
    ui.column(.{ .gap = 6 }, .{
        ui.text(.{}, "Build output"),
        ui.text(.{ .wrap = true, .style_tokens = .{ .foreground = .text_muted } }, "Build completed."),
    }),
})
```

## Attributes

| Attribute  | Description                                                                                                                                                                                                  |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `padding`  | Uniform padding (plain number).                                                                                                                                                                              |
| `width`    | Definite width (plain number): the element is exactly this wide; content neither shrinks nor overflows it. On resizable it is the initial width.                                                             |
| `on-press` | Dispatch a Msg on press: tag or tag:\{payload}. Legal on any element — a bound press handler makes it pressable, and presses on plain text/icons inside it fall through to it (dragging still selects text). |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Progress
url: "https://native-sdk.dev/docs/components/progress"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `progress` to display a fraction from 0 to 1. The renderer clamps values outside that range. Set `width` or `grow` to size the track. The control has no input events. Use [slider](/docs/components/slider) for an adjustable value or [spinner](/docs/components/spinner) for an indeterminate wait.

## Markup

```html
<progress value="{upload_fraction}" width="280" />
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.el(.progress, .{ .value = model.upload_fraction, .width = 280 }, .{})
```

## Attributes

| Attribute | Description                                                                                                                                      |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `value`   | Value for slider/progress/text entry; a literal or one \{binding}.                                                                               |
| `width`   | Definite width (plain number): the element is exactly this wide; content neither shrinks nor overflows it. On resizable it is the initial width. |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Radio
url: "https://native-sdk.dev/docs/components/radio"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `radio` inside a `radio-group` to select one option. Give the group an accessible `label`, and provide each option’s visible label as content or `text`. Bind `checked` to the model’s choice.

Descendant radios form one group regardless of nesting depth. The group has one Tab stop; arrow keys wrap through choices, and Home/End move to the first or last option. Focus and selection move together. A selection change dispatches `on-change` when bound, otherwise `on-toggle` or `on-press`. Activating an already selected radio does not dispatch `on-change`; legacy fallback handlers still receive the activation.

## Markup

```html
<radio-group gap="12" label="Density">
  <radio checked="{density == 'default'}" on-change="set_default">Default</radio>
  <row>
    <radio checked="{density == 'comfortable'}" on-change="set_comfortable">Comfortable</radio>
  </row>
  <radio checked="{density == 'compact'}" disabled="true">Compact</radio>
</radio-group>
```

One field holds the group's choice — a string-literal union in a TypeScript core, an enum in a Zig core — and each radio's arm sets it:

```ts
// model: { readonly density: "default" | "comfortable" | "compact" }
case "set_comfortable":
  return { ...model, density: "comfortable" };
```

```zig
// model: density: enum { default, comfortable, compact } = .default,
.set_comfortable => model.density = .comfortable,
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.el(.radio_group, .{ .gap = 12, .semantics = .{ .label = "Density" } }, .{
    ui.el(.radio, .{ .text = "Default", .checked = model.density == .default, .on_change = .set_default }, .{}),
    ui.row(.{}, .{
        ui.el(.radio, .{ .text = "Comfortable", .checked = model.density == .comfortable, .on_change = .set_comfortable }, .{}),
    }),
    ui.el(.radio, .{ .text = "Compact", .checked = model.density == .compact, .disabled = true }, .{}),
})
```

## Attributes

| Attribute   | Description                                                                                                                                                                                                                  |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`      | Text value for text-bearing elements; a literal or one \{binding}.                                                                                                                                                           |
| `checked`   | Checked state for checkbox/toggle; true/false or a \{binding}.                                                                                                                                                               |
| `disabled`  | Disables the control; true/false or a \{binding}.                                                                                                                                                                            |
| `on-change` | Dispatch a Msg on change: tag or tag:\{payload}. Hit-target elements only. On a treeitem, Up/Down/Left/Right/Home/End focus movement prefers on-change over on-press, separating keyboard selection from pointer activation. |
| `on-toggle` | Dispatch a Msg on toggle: tag or tag:\{payload}. Hit-target elements only (checkbox, toggle, toggle-button, switch, accordion, ...).                                                                                         |
| `on-press`  | Dispatch a Msg on press: tag or tag:\{payload}. Legal on any element — a bound press handler makes it pressable, and presses on plain text/icons inside it fall through to it (dragging still selects text).                 |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Resizable
url: "https://native-sdk.dev/docs/components/resizable"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `resizable` for a panel with a drag handle. `width` sets its initial width; the engine retains the width after dragging. Resizing affects the panel’s frame, while children and neighboring widgets keep their frames. Children stack, so use an inner row or column for flow. Use [split](/docs/components/split) when the model should own the divider position or two panes should resize together.

## Markup

```html
<row grow="1">
  <resizable width="260">
    <column padding="12">
      <text foreground="text_muted">Sidebar</text>
    </column>
  </resizable>
</row>
```

## Width and retained state

`width` and `min-width` size a newly mounted panel. An enabled panel keeps its user-adjusted width across model updates, including changes to those attributes. A removed panel or a fresh key starts from the authored size again. Disabled panels use their authored size and ignore dragging.

During a drag, the minimum width is the greater of 48 pixels and the panel’s height. Resizing changes the panel’s frame; its children and neighboring widgets keep their own frames.

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.row(.{ .grow = 1 }, .{
    ui.el(.resizable, .{ .width = 260 }, .{
        ui.column(.{ .padding = 12 }, .{
            ui.text(.{ .style_tokens = .{ .foreground = .text_muted } }, "Sidebar"),
        }),
    }),
})
```

## Attributes

| Attribute   | Description                                                                                                                                              |
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `width`     | Definite width (plain number): the element is exactly this wide; content neither shrinks nor overflows it. On resizable it is the initial width.         |
| `min-width` | Width floor (plain number) without width's definite max: the element may grow past it but never shrink below. On split panes it bounds the divider drag. |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Scroll
url: "https://native-sdk.dev/docs/components/scroll"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `scroll` for a viewport whose content exceeds its bounds. Wrap multiple children in one column. The engine handles wheel, kinetic, and keyboard scrolling and displays a scrollbar during scrolling. `on-scroll` delivers offsets, velocities, and viewport and content extents for both axes. In TypeScript, import `ScrollState` from `@native-sdk/core/events` and use camelCase fields such as `offsetY`. Zig uses `canvas.ScrollState` with snake\_case fields such as `offset_y`.

Set `axis` to `vertical` (the default), `horizontal`, or `both`. Horizontal regions handle horizontal wheel input, display a bottom scrollbar, and support Left/Right keys. Horizontal-only regions also use Home/End/PageUp/PageDown on that axis. Bind `value` for the vertical offset and `value-x` for the horizontal offset to control position from the model.

Nested regions route each wheel axis to the nearest ancestor that supports it. For example, a vertical list inside a horizontal timeline handles vertical input in the list and horizontal input in the timeline. Virtualized scroll regions support only the vertical axis.

Scrolling stops at content edges by default. Set `overscroll="rubber_band"` to allow a region to bounce past its edges. `ScrollPhysics.overscroll` sets the app-wide default; a region’s value overrides it.

`on-reach-end` dispatches a message when the offset comes within one viewport of the content end. It fires once per approach. Adding content can re-arm it; otherwise re-arming requires a post-scroll observation at least 1.5 viewports from the end. A programmatic jump to the end fires once. Use this event to request the next batch of content. Pair it with [list](/docs/components/list) to limit layout and painting, or the Zig builder’s [virtual list](/docs/components/virtual-list) to build only the visible rows.

## Markup

```html
<scroll height="240" padding="8" on-scroll="log_scrolled">
  <column gap="2">
    <list-item on-press="open_entry">Changelog entry 14</list-item>
    <list-item on-press="open_entry">Changelog entry 13</list-item>
    <list-item on-press="open_entry">Changelog entry 12</list-item>
    <list-item on-press="open_entry">Changelog entry 11</list-item>
    <list-item on-press="open_entry">Changelog entry 10</list-item>
    <list-item on-press="open_entry">Changelog entry 9</list-item>
    <list-item on-press="open_entry">Changelog entry 8</list-item>
  </column>
</scroll>
```

The arm receives the scroll state and stores what the model wants to remember — echo `offsetY` into a field bound as the scroll's `value` (and `offsetX` into `value-x` on horizontal-capable regions) and the model owns the position too:

```ts
import { type ScrollState } from "@native-sdk/core/events";

export type Msg = /* ... */ | { readonly kind: "log_scrolled"; readonly scroll: ScrollState };
// in update:
case "log_scrolled":
  return { ...model, changelog_offset: msg.scroll.offsetY };
```

```zig
// Msg arm: log_scrolled: canvas.ScrollState
.log_scrolled => |scroll| model.changelog_offset = scroll.offset_y,
```

A horizontal shelf declares its axis and reads the `X` fields:

```html
<scroll axis="horizontal" height="180" on-scroll="shelf_scrolled">
  <row gap="8">
    <panel width="140"><text>Cover 1</text></panel>
    <panel width="140"><text>Cover 2</text></panel>
    <panel width="140"><text>Cover 3</text></panel>
  </row>
</scroll>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically. `on_scroll` pairs with `Ui.scrollMsg(.tag)`; the delivered offset is the value the runtime already applied, so echoing it back never fights the scroll reconcile.

```zig
ui.scroll(.{ .height = 240, .padding = 8, .on_scroll = Ui.scrollMsg(.log_scrolled) }, .{
    ui.column(.{ .gap = 2 }, .{
        ui.listItem(.{ .on_press = .open_entry }, "Changelog entry 14"),
        ui.listItem(.{ .on_press = .open_entry }, "Changelog entry 13"),
        ui.listItem(.{ .on_press = .open_entry }, "Changelog entry 12"),
        ui.listItem(.{ .on_press = .open_entry }, "Changelog entry 11"),
        ui.listItem(.{ .on_press = .open_entry }, "Changelog entry 10"),
    }),
})
```

## Attributes

| Attribute      | Description                                                                                                                                                                                                                                                                                                                                                                                        |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `on-scroll`    | scroll element only: names a Msg variant with canvas.ScrollState payload; delivers the post-scroll two-axis state (offset\_x/offset\_y, velocity\_x/velocity\_y, viewport\_extent\_x/viewport\_extent\_y, content\_extent\_x/content\_extent\_y) after wheel, kinetic, keyboard, and accessibility scrolls.                                                                                        |
| `on-reach-end` | scroll element only: Msg (tag or tag:\{payload}) dispatched when a user scroll comes within one viewport of the content end - the infinite-scroll fetch signal. Fires once per approach with hysteresis: it re-arms only after the offset retreats past 1.5 viewports, which appending a batch causes on its own by growing the extent.                                                            |
| `axis`         | scroll only: which axes the region scrolls - vertical (the default), horizontal, or both. Horizontal grants opt the region into wheel/trackpad delta-x, the bottom-edge scrollbar, and the horizontal keymap; in a nested tree each wheel axis routes independently to the nearest ancestor scrolling that axis. Virtualized scrolls stay vertical (a horizontal grant there is a teaching error). |
| `value-x`      | scroll only, beside axis="horizontal" or axis="both": the horizontal scroll offset - the sideways counterpart of value, following the same source-wins reconcile rule (echo on-scroll's offset\_x back to keep user scrolling; move it model-side to scroll programmatically). Without a horizontal axis grant it is a teaching error (it would be silently inert).                                |
| `overscroll`   | scroll only: edge behavior of the region. none pins scrolling at the content edges (the shipped default via the ScrollPhysics.overscroll token), rubber\_band lets this region bounce past them, default follows the token. Honored by the engine's scroll physics and the native OS scroller alike.                                                                                               |
| `height`       | Definite height (plain number): the element is exactly this tall; content neither shrinks nor overflows it.                                                                                                                                                                                                                                                                                        |
| `width`        | Definite width (plain number): the element is exactly this wide; content neither shrinks nor overflows it. On resizable it is the initial width.                                                                                                                                                                                                                                                   |
| `padding`      | Uniform padding (plain number).                                                                                                                                                                                                                                                                                                                                                                    |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Segmented Control
url: "https://native-sdk.dev/docs/components/segmented-control"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `segmented-control` for one item in an exclusive-choice control. Bind `selected` to the model and dispatch `on-press` to change it. Arrange several segments in a row. Each can include an `icon`. Use [tabs](/docs/components/tabs) for a tab-strip container with conditional panels.

## Markup

```html
<row gap="6">
  <segmented-control selected="{range == 'day'}" on-press="choose_day">Day</segmented-control>
  <segmented-control selected="{range == 'week'}" icon="settings" on-press="choose_week">Week</segmented-control>
  <segmented-control selected="{range == 'month'}" on-press="choose_month">Month</segmented-control>
</row>
```

`selected` is model-owned. Pressing a segment sends its message; the next view rebuild reflects the new selection. The segment itself is a text-bearing control, so it needs visible content or a `label` accessible name.

## Icons and sizes

The `icon` attribute uses the same closed vector-icon vocabulary as [button](/docs/components/button). It is drawn as part of the segment's own hit target and follows the segment's state tint. `size` selects the control size; `label` supplies an accessible name when the visible content is absent.

```html
<row gap="6">
  <segmented-control size="sm" selected="{view == 'list'}" icon="file-text" label="List" on-press="show_list" />
  <segmented-control size="sm" selected="{view == 'board'}" icon="settings" label="Board" on-press="show_board" />
</row>
```

## Programmatic construction (Zig)

In a Zig view, use the same retained widget kind directly with `canvas.Ui`:

```zig
ui.row(.{ .gap = 6 }, .{
    ui.el(.segmented_control, .{ .text = "Day", .selected = model.range == .day, .on_press = .choose_day }, .{}),
    ui.el(.segmented_control, .{ .text = "Week", .icon = "settings", .selected = model.range == .week, .on_press = .choose_week }, .{}),
    ui.el(.segmented_control, .{ .text = "Month", .selected = model.range == .month, .on_press = .choose_month }, .{}),
})
```

## Attributes

| Attribute        | Description                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`           | Text value for text-bearing elements; a literal or one \{binding}.                                                                                                                                                                                                                                                                                                                                                                              |
| `selected`       | Selected state; often a \{a == b} equality.                                                                                                                                                                                                                                                                                                                                                                                                     |
| `disabled`       | Disables the control; true/false or a \{binding}.                                                                                                                                                                                                                                                                                                                                                                                               |
| `size`           | Control size: default\|sm\|lg\|icon. On text, also the typography rungs heading\|display (themable token steps above title - a section heading, a hero stat); numeric sizes are not accepted - retheme the typography tokens instead.                                                                                                                                                                                                           |
| `icon`           | button, toggle-button, list-item, menu-item: vector icon drawn inline (buttons/toggle-buttons before the label, list/menu items as a leading slot): a built-in name (comptime-validated against canvas.icons.known\_icon\_names, e.g. save, plus, refresh-cw), an app-registered app:\<name>, or one \{binding} resolving to such a name. Icon-only buttons when the content is empty — add a label. One hit target, one enabled/disabled tint. |
| `icon-placement` | Icon slot side on label-bearing buttons/toggle-buttons: leading (default) draws the icon before the label, trailing after it — the next-page chevron. Icon-only buttons center the glyph regardless.                                                                                                                                                                                                                                            |
| `label`          | Accessible name; when set it REPLACES the element's text as the announced name - screen readers and automation snapshots see the label, never the text it shadows.                                                                                                                                                                                                                                                                              |
| `key`            | Sibling-scoped identity key; on for, names an item field.                                                                                                                                                                                                                                                                                                                                                                                       |
| `global-key`     | Parent-independent identity: ids survive reparenting between containers.                                                                                                                                                                                                                                                                                                                                                                        |
| `on-press`       | Dispatch a Msg on press: tag or tag:\{payload}. Legal on any element — a bound press handler makes it pressable, and presses on plain text/icons inside it fall through to it (dragging still selects text).                                                                                                                                                                                                                                    |
| `on-hold`        | Press-and-hold Msg: a pointer held \~350 ms dispatches it and the release then presses nothing; a quick click dispatches on-press as usual. A right/ctrl-click with no context menu on the route dispatches it immediately. Like on-press, binding it makes any element pressable.                                                                                                                                                              |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Select
url: "https://native-sdk.dev/docs/components/select"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `select` as a trigger for a choice menu. Its content displays the current value; `placeholder` displays when it is empty. Handle `on-press` to open an anchored [dropdown-menu](/docs/components/dropdown-menu) containing `menu-item` options. Place the trigger and conditional menu in a `stack`. The model owns the open flag and clears it on selection or `on-dismiss`.

The selected option has a checkmark. Keyboard and pointer focus use a separate highlight, so moving through the menu does not change the committed value. For text entry with suggestions, see [combobox](/docs/components/combobox).

## Markup

Place the trigger and menu in a `stack`. Set `anchor="below"` and `anchor-alignment="stretch"` to align the menu below the trigger at the same width. Both `pick_env` and `close_env_picker` clear `env_picker_open` in `update`.

Enter, Space, or an arrow key activates the focused trigger. In the menu, Up/Down move through options from the selected item; Home/End move to the first or last. Enter or a click selects an option. Escape, Tab, or a click outside dispatches `on-dismiss` without changing the value. Focus returns to the trigger.

```html
<stack width="240">
  <select placeholder="Choose environment" on-press="toggle_env_picker">{envName}</select>
  <if test="{env_picker_open}">
    <dropdown-menu anchor="below" anchor-alignment="stretch" on-dismiss="close_env_picker">
      <for each="environments" key="id" as="e">
        <menu-item selected="{e.id == env_id}" on-press="pick_env:{e.id}">{e.name}</menu-item>
      </for>
    </dropdown-menu>
  </if>
</stack>
```

The core side is the open flag, the committed choice, and three arms:

```ts
// model: { readonly env_id: number; readonly env_picker_open: boolean }
case "toggle_env_picker":
  return { ...model, env_picker_open: !model.env_picker_open };
case "close_env_picker":
  return { ...model, env_picker_open: false };
case "pick_env":
  return { ...model, env_id: msg.id, env_picker_open: false };
```

```zig
.toggle_env_picker => model.env_picker_open = !model.env_picker_open,
.close_env_picker => model.env_picker_open = false,
.pick_env => |id| {
    model.env_id = id;
    model.env_picker_open = false;
},
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.stack(.{ .width = 240 }, .{
    ui.el(.select, .{
        .text = model.envName(),
        .placeholder = "Choose environment",
        .on_press = .toggle_env_picker,
    }, .{}),
    if (model.env_picker_open) ui.el(.dropdown_menu, .{
        .anchor = .below,
        .anchor_alignment = .stretch,
        .on_dismiss = .close_env_picker,
    }, .{
        ui.el(.menu_item, .{ .text = "Production", .selected = model.env_id == 0, .on_press = .{ .pick_env = 0 } }, .{}),
        ui.el(.menu_item, .{ .text = "Staging", .selected = model.env_id == 1, .on_press = .{ .pick_env = 1 } }, .{}),
        ui.el(.menu_item, .{ .text = "Development", .selected = model.env_id == 2, .on_press = .{ .pick_env = 2 } }, .{}),
    }) else ui.spacer(0),
})
```

## Attributes

| Attribute     | Description                                                                                                                                                                                                                                                                                                   |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`        | Text value for text-bearing elements; a literal or one \{binding}.                                                                                                                                                                                                                                            |
| `placeholder` | Hint text shown while a text entry is empty.                                                                                                                                                                                                                                                                  |
| `disabled`    | Disables the control; true/false or a \{binding}.                                                                                                                                                                                                                                                             |
| `on-press`    | Dispatch a Msg on press: tag or tag:\{payload}. Legal on any element — a bound press handler makes it pressable, and presses on plain text/icons inside it fall through to it (dragging still selects text).                                                                                                  |
| `on-dismiss`  | Dismissible surfaces only (dialog, drawer, sheet, dropdown-menu): Msg dispatched when Escape or a click outside dismisses the surface, so the MODEL owns the close (clear the open flag in update). The engine hides the surface immediately as an optimistic echo; the source tree wins on the next rebuild. |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Separator
url: "https://native-sdk.dev/docs/components/separator"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

A layout leaf that draws a line: a horizontal rule in a column, a thin vertical divider in a row (give the vertical form an explicit `width` and `height`). For flexible empty space between siblings, see [spacer](/docs/components/spacer).

## Markup

```html
<column gap="12" width="300">
  <text>Native SDK</text>
  <text foreground="text_muted">A component catalog rendered by the engine.</text>
  <separator></separator>
  <row gap="12" cross="center" height="20">
    <text>Docs</text>
    <separator width="1" height="16"></separator>
    <text>Source</text>
    <separator width="1" height="16"></separator>
    <text>Changelog</text>
  </row>
</column>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.column(.{ .gap = 12, .width = 300 }, .{
    ui.text(.{}, "Native SDK"),
    ui.text(.{ .style_tokens = .{ .foreground = .text_muted } }, "A component catalog rendered by the engine."),
    ui.separator(.{}),
    ui.row(.{ .gap = 12, .cross = .center, .height = 20 }, .{
        ui.text(.{}, "Docs"),
        ui.separator(.{ .width = 1, .height = 16 }),
        ui.text(.{}, "Source"),
        ui.separator(.{ .width = 1, .height = 16 }),
        ui.text(.{}, "Changelog"),
    }),
})
```

## Attributes

| Attribute | Description                                                                                                                                      |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `width`   | Definite width (plain number): the element is exactly this wide; content neither shrinks nor overflows it. On resizable it is the initial width. |
| `height`  | Definite height (plain number): the element is exactly this tall; content neither shrinks nor overflows it.                                      |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Sheet
url: "https://native-sdk.dev/docs/components/sheet"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `sheet` for a modal surface aligned to the right edge of the root view. It spans the root height regardless of its parent container. Set `width` to size it, render it inside a model-bound `if`, and clear the open flag on `on-dismiss` after Escape or a click outside. Children stack over the content box; begin an inner body column with a spacer to leave room for the title. See [dialog](/docs/components/dialog) and [drawer](/docs/components/drawer) for other placements.

## Markup

```html
<column padding="24">
  <button variant="outline" icon="external-link" on-press="open_share">Share</button>
  <if test="{share_open}">
    <sheet text="Share" width="320" padding="24" on-dismiss="close_share">
      <column gap="12">
        <spacer height="34"></spacer>
        <text wrap="true" foreground="text_muted">Anyone with the link can view this board.</text>
        <row gap="8">
          <input text="{share_url}" label="Share link" grow="1"></input>
          <button variant="secondary" icon="copy" on-press="copy_link">Copy</button>
        </row>
      </column>
    </sheet>
  </if>
</column>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.el(.sheet, .{ .text = "Share", .width = 320, .padding = 24, .on_dismiss = .close_share }, .{
    ui.column(.{ .gap = 12 }, .{
        ui.column(.{ .height = 34 }, .{}), // clears the engine-drawn title line
        ui.text(.{ .wrap = true, .style_tokens = .{ .foreground = .text_muted } }, "Anyone with the link can view this board."),
        ui.row(.{ .gap = 8 }, .{
            ui.el(.input, .{ .text = model.share_url, .grow = 1, .semantics = .{ .label = "Share link" } }, .{}),
            ui.button(.{ .variant = .secondary, .icon = "copy", .on_press = .copy_link }, "Copy"),
        }),
    }),
})
```

## Attributes

The sheet rejects `gap` — it layers children like the other stacking surfaces; put a column (or row) inside for flow.

| Attribute    | Description                                                                                                                                                                                                                                                                                                   |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`       | Text value for text-bearing elements; a literal or one \{binding}.                                                                                                                                                                                                                                            |
| `width`      | Definite width (plain number): the element is exactly this wide; content neither shrinks nor overflows it. On resizable it is the initial width.                                                                                                                                                              |
| `height`     | Definite height (plain number): the element is exactly this tall; content neither shrinks nor overflows it.                                                                                                                                                                                                   |
| `padding`    | Uniform padding (plain number).                                                                                                                                                                                                                                                                               |
| `on-dismiss` | Dismissible surfaces only (dialog, drawer, sheet, dropdown-menu): Msg dispatched when Escape or a click outside dismisses the surface, so the MODEL owns the close (clear the open flag in update). The engine hides the surface immediately as an optimistic echo; the source tree wins on the next rebuild. |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Skeleton
url: "https://native-sdk.dev/docs/components/skeleton"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `skeleton` as a placeholder while content loads. Set `width` and `height` to approximate the content, then replace it through a model-bound `if`. The engine animates its opacity; reduced-motion settings make it static. Use [spinner](/docs/components/spinner) for an indeterminate wait or [progress](/docs/components/progress) for a measurable fraction.

## Markup

```html
<row gap="12" width="320">
  <skeleton width="44" height="44"></skeleton>
  <column gap="8" grow="1" main="center">
    <skeleton height="14"></skeleton>
    <skeleton height="14" width="180"></skeleton>
  </column>
</row>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically. Skeleton has no sugar method; use `ui.el` with the element kind.

```zig
ui.row(.{ .gap = 12, .width = 320 }, .{
    ui.el(.skeleton, .{ .width = 44, .height = 44 }, .{}),
    ui.column(.{ .gap = 8, .grow = 1, .main = .center }, .{
        ui.el(.skeleton, .{ .height = 14 }, .{}),
        ui.el(.skeleton, .{ .height = 14, .width = 180 }, .{}),
    }),
})
```

## Attributes

| Attribute | Description                                                                                                                                      |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `width`   | Definite width (plain number): the element is exactly this wide; content neither shrinks nor overflows it. On resizable it is the initial width. |
| `height`  | Definite height (plain number): the element is exactly this tall; content neither shrinks nor overflows it.                                      |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Slider
url: "https://native-sdk.dev/docs/components/slider"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `slider` to adjust a fraction from 0 to 1. Bind `value` to the model and handle `on-change` after dragging, keyboard adjustments, or an accessibility set-value action. A message with one numeric payload receives the applied fraction; a message without a payload reports only that a change occurred.

For controlled state, store the fraction in `update` and bind it back to `value`. A changed model value overrides retained state. Repeating the same source value preserves a user adjustment, and a live drag is not interrupted by reconciliation. For a display-only bar, use [progress](/docs/components/progress).

## Markup

```html
<column gap="20" width="280">
  <slider value="{volume}" on-change="volume_changed" label="Volume" />
  <slider value="0.7" disabled="true" label="Balance" />
</column>
```

With a one-number float arm — `{ kind: "volume_changed"; fraction: number }` in a TypeScript core, `volume_changed: f32` in a Zig core — the arm receives the applied fraction directly (the `examples/soundboard` seek bar is the live pattern):

```ts
// in update: the value event delivers the applied fraction
case "volume_changed":
  return { ...model, volume: msg.fraction };
```

```zig
// in update: the value event delivers the applied fraction
.volume_changed => |fraction| model.volume = fraction,
```

A void `volume_changed` arm keeps the older plain form; the `Options.sync` hook remains available to Zig apps that mirror runtime widget state wholesale.

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically; `on_value = Ui.valueMsg(.tag)` is the value channel:

```zig
ui.column(.{ .gap = 20, .width = 280 }, .{
    ui.el(.slider, .{
        .value = model.volume,
        .on_value = Ui.valueMsg(.volume_changed),
    }, .{}),
    ui.el(.slider, .{ .value = 0.7, .disabled = true, .semantics = .{ .label = "Balance" } }, .{}),
})
```

## Attributes

| Attribute   | Description                                                                                                                                                                                                                  |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `value`     | Value for slider/progress/text entry; a literal or one \{binding}.                                                                                                                                                           |
| `disabled`  | Disables the control; true/false or a \{binding}.                                                                                                                                                                            |
| `on-change` | Dispatch a Msg on change: tag or tag:\{payload}. Hit-target elements only. On a treeitem, Up/Down/Left/Right/Home/End focus movement prefers on-change over on-press, separating keyboard selection from pointer activation. |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Spacer
url: "https://native-sdk.dev/docs/components/spacer"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `spacer` for empty space between siblings. `grow` fills unused space on the main axis, which can push toolbar actions to the trailing edge. Set `width` or `height` for a fixed gap. Use [separator](/docs/components/separator) for a visible dividing line.

## Markup

```html
<panel width="340" padding="12">
  <row gap="12" cross="center">
    <text>Docs</text>
    <text foreground="text_muted">Source</text>
    <spacer grow="1"></spacer>
    <button size="sm" variant="secondary" on-press="open_changelog">Changelog</button>
  </row>
</panel>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically. `ui.spacer(1)` is the builder shorthand for a grow-1 spacer.

```zig
ui.panel(.{ .width = 340, .padding = 12 }, .{
    ui.row(.{ .gap = 12, .cross = .center }, .{
        ui.text(.{}, "Docs"),
        ui.text(.{ .style_tokens = .{ .foreground = .text_muted } }, "Source"),
        ui.spacer(1),
        ui.button(.{ .size = .sm, .variant = .secondary, .on_press = .open_changelog }, "Changelog"),
    }),
})
```

## Attributes

| Attribute | Description                                                                                                                                      |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `width`   | Definite width (plain number): the element is exactly this wide; content neither shrinks nor overflows it. On resizable it is the initial width. |
| `height`  | Definite height (plain number): the element is exactly this tall; content neither shrinks nor overflows it.                                      |
| `grow`    | Flex grow factor; give spacer one.                                                                                                               |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Spinner
url: "https://native-sdk.dev/docs/components/spinner"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

An indeterminate progress leaf for work with no measurable fraction; when there is one, use [progress](/docs/components/progress) instead. For a placeholder that sketches the shape of the loading content, use [skeleton](/docs/components/skeleton).

## Markup

```html
<row gap="12" cross="center">
  <spinner></spinner>
  <text foreground="text_muted">Loading notes…</text>
</row>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically. Spinner has no sugar method; use `ui.el` with the element kind.

```zig
ui.row(.{ .gap = 12, .cross = .center }, .{
    ui.el(.spinner, .{}, .{}),
    ui.text(.{ .style_tokens = .{ .foreground = .text_muted } }, "Loading notes…"),
})
```

## Attributes

| Attribute | Description                                                                                                                                      |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `width`   | Definite width (plain number): the element is exactly this wide; content neither shrinks nor overflows it. On resizable it is the initial width. |
| `height`  | Definite height (plain number): the element is exactly this tall; content neither shrinks nor overflows it.                                      |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Split
url: "https://native-sdk.dev/docs/components/split"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `split` for two horizontal panes separated by a draggable divider. It requires exactly two element children. Bind `value` to the first pane’s fraction and handle `on-resize` with a numeric payload (`number` in TypeScript, `f32` in Zig). Store the applied fraction in `update`.

The event fires after dragging, keyboard adjustments, or accessibility increments. Pane `min-width` values limit the divider range; `gap` sets its thickness. Nest splits for more panes. Use [resizable](/docs/components/resizable) for a single panel whose width is retained by the engine.

## Markup

```html
<split value="{sidebar_fraction}" on-resize="sidebar_resized" gap="8" grow="1">
  <panel padding="12" min-width="180">
    <text foreground="text_muted">Sidebar</text>
  </panel>
  <panel padding="12" min-width="320">
    <text foreground="text_muted">Content</text>
  </panel>
</split>
```

A `value` of 0 lays out at 0.5, so a fresh model starts centered. Put conditional or repeated content inside a pane container — the split itself takes exactly two children.

In `update`, the arm echoes the applied fraction back into the bound field:

```ts
case "sidebar_resized":
  return { ...model, sidebar_fraction: msg.fraction };
```

```zig
.sidebar_resized => |fraction| model.sidebar_fraction = fraction,
```

## Animated resize

Set `resize-duration` in milliseconds to animate changes to the bound fraction. Both panes reflow during the animation, and `on-resize` reports the applied fraction. `resize-easing` accepts `linear`, `standard` (default), `emphasized`, or `spring` and requires a nonzero duration. Reduced-motion settings and a zero or omitted duration apply the target immediately.

```html
<split value="{sidebar_fraction}" resize-duration="180" resize-easing="standard" on-resize="sidebar_resized" gap="8" grow="1">
  <panel padding="12" min-width="180">
    <text foreground="text_muted">Sidebar</text>
  </panel>
  <panel padding="12" min-width="320">
    <text foreground="text_muted">Content</text>
  </panel>
</split>
```

Here `sidebar_fraction` is the model's declared resting fraction — flip it between the expanded and collapsed values in `update` and the runtime animates the divider there. In Zig views the same pair is `ElementOptions.resize_duration`/`resize_easing` on `ui.split`.

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically. `on_resize` pairs with `valueMsg`, which builds the `f32`-payload message constructor.

```zig
ui.split(.{
    .value = model.sidebar_fraction,
    .on_resize = Ui.valueMsg(.sidebar_resized),
    .gap = 8,
    .grow = 1,
}, .{
    ui.panel(.{ .padding = 12, .min_width = 180 }, .{
        ui.text(.{ .style_tokens = .{ .foreground = .text_muted } }, "Sidebar"),
    }),
    ui.panel(.{ .padding = 12, .min_width = 320 }, .{
        ui.text(.{ .style_tokens = .{ .foreground = .text_muted } }, "Content"),
    }),
})
```

## Attributes

| Attribute         | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `value`           | Value for slider/progress/text entry; a literal or one \{binding}.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `resize-duration` | split only: layout-tween duration in milliseconds (a plain number or one \{binding}). Nonzero makes the bound value a TARGET - a rebuild that moves it lays both panes out at the target ONCE, then the runtime slides the rendered boundary there one presented frame at a time under the panes' clips (content never re-wraps mid-flight), dispatching ONE on-resize echo at settle with the applied fraction. 0 (and absent) snaps, today's behavior. A divider DRAG keeps live per-step reflow and echoes. Reduced-motion appearances snap automatically - apps declare nothing extra. |
| `resize-easing`   | split only, beside a nonzero resize-duration: easing curve of the layout tween - linear, standard (the default), emphasized, or spring. Easing without a duration is a teaching error (it would be silently inert).                                                                                                                                                                                                                                                                                                                                                                        |
| `min-width`       | Width floor (plain number) without width's definite max: the element may grow past it but never shrink below. On split panes it bounds the divider drag.                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `gap`             | Spacing between children (plain number). Rejected on stacking containers (stack, panel, card, the modal surfaces) — they layer children; put a column (or row) inside for flow.                                                                                                                                                                                                                                                                                                                                                                                                            |
| `on-resize`       | split element only: names a Msg variant with f32 payload; delivers the applied first-pane fraction after every divider drag, keyboard adjustment, and assistive increment/decrement. Echo it back into value - the delivered fraction never fights the reconcile.                                                                                                                                                                                                                                                                                                                          |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Status Bar
url: "https://native-sdk.dev/docs/components/status-bar"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `status-bar` for a text status line. It accepts text content with model interpolation and no child elements. Place it after a growing content area in the root column to position it at the bottom of the window.

## Markup

```html
<column grow="1">
  <column grow="1" padding="32" main="center" cross="center">
    <text foreground="text_muted">Window content</text>
  </column>
  <status-bar>Ready — 3 notes synced</status-bar>
</column>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically. The builder models it as a text leaf: `ui.statusBar(options, text)`.

```zig
ui.column(.{ .grow = 1 }, .{
    ui.column(.{ .grow = 1, .padding = 32, .main = .center, .cross = .center }, .{
        ui.text(.{ .style_tokens = .{ .foreground = .text_muted } }, "Window content"),
    }),
    ui.statusBar(.{}, "Ready — 3 notes synced"),
})
```

## Attributes

| Attribute        | Description                                                                                                                                                                                                                                                                                   |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`           | Text value for text-bearing elements; a literal or one \{binding}.                                                                                                                                                                                                                            |
| `text-alignment` | Horizontal alignment of text content: start\|center\|end. Consumed by text (plain and wrapped), status-bar, and surface titles; controls that own their label placement (button, badge) ignore it. On a bubble's reactions child it names the pill's dock (end is the default trailing dock). |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Stepper
url: "https://native-sdk.dev/docs/components/stepper"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `stepper` to display progress through stages. Its `step` children contain text labels. `active` identifies the current index: earlier stages display a checkmark, later stages remain pending, and an index beyond the last stage marks all stages complete. The model controls `active`; the stepper has no input events. For vertical events, see [timeline](/docs/components/timeline).

## Markup

`active` is required — a number or one `{binding}`. The component is also ejectable as a markup template when you need to own its shape.

```html
<stepper active="{publish_step}">
  <step>Draft</step>
  <step>Review</step>
  <step>Publish</step>
</stepper>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.stepper(.{ .active = model.publish_step }, &.{
    .{ .label = "Draft" },
    .{ .label = "Review" },
    .{ .label = "Publish" },
})
```

## Eject

Use the eject command to copy stepper into your project when you need to change its structure. SDK updates preserve your copy. The command refuses to overwrite an existing file.

`stepper` ejects as a markup template (`src/components/stepper.native`).

```sh
native eject component stepper
```

The ownership model and what to do after ejecting are in [Use, eject, or build](/docs/building-components#use-eject-or-build).

## Attributes

| Attribute    | Description                                                                                                                                                        |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `active`     | stepper: the active step index (a number or one \{binding}); earlier steps render completed, later ones pending. Required.                                         |
| `key`        | Sibling-scoped identity key.                                                                                                                                       |
| `global-key` | Parent-independent identity: ids survive reparenting between containers.                                                                                           |
| `label`      | Accessible name; when set it REPLACES the element's text as the announced name - screen readers and automation snapshots see the label, never the text it shadows. |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Switch
url: "https://native-sdk.dev/docs/components/switch"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `switch` for a boolean setting. Provide a label as element content, bind `checked` to the model, and handle `on-toggle`. It uses the same state contract as [checkbox](/docs/components/checkbox) with a sliding thumb.

## Markup

```html
<column gap="12">
  <switch checked="{airplane_mode}" on-toggle="toggle_airplane_mode">Airplane mode</switch>
  <switch checked="{notifications}" on-toggle="toggle_notifications">Notifications</switch>
  <switch checked="true" disabled="true">Managed setting</switch>
</column>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically — the widget kind is `.switch_control`:

```zig
ui.column(.{ .gap = 12 }, .{
    ui.el(.switch_control, .{ .text = "Airplane mode", .checked = model.airplane_mode, .on_toggle = .toggle_airplane_mode }, .{}),
    ui.el(.switch_control, .{ .text = "Notifications", .checked = model.notifications, .on_toggle = .toggle_notifications }, .{}),
    ui.el(.switch_control, .{ .text = "Managed setting", .checked = true, .disabled = true }, .{}),
})
```

## Attributes

| Attribute   | Description                                                                                                                          |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `text`      | Text value for text-bearing elements; a literal or one \{binding}.                                                                   |
| `checked`   | Checked state for checkbox/toggle; true/false or a \{binding}.                                                                       |
| `disabled`  | Disables the control; true/false or a \{binding}.                                                                                    |
| `on-toggle` | Dispatch a Msg on toggle: tag or tag:\{payload}. Hit-target elements only (checkbox, toggle, toggle-button, switch, accordion, ...). |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Table
url: "https://native-sdk.dev/docs/components/table"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `table` for aligned columns of text. It contains `table-row` elements, each containing `table-cell` leaves; the validator checks this nesting. Give corresponding cells matching `grow` factors to align columns.

Rows have separators and a hover highlight. Bind `selected` to highlight a row, `text-alignment="end"` to right-align numeric cells, and `on-press` to handle cell activation.

## Markup

```html
<table width="420">
  <table-row>
    <table-cell grow="1" size="sm" foreground="text_muted">Invoice</table-cell>
    <table-cell grow="1" size="sm" foreground="text_muted">Status</table-cell>
    <table-cell grow="1" size="sm" foreground="text_muted" text-alignment="end">Amount</table-cell>
  </table-row>
  <for each="invoices" as="invoice" key="id">
    <table-row selected="{invoice.id == selected_invoice}">
      <table-cell grow="1" on-press="open_invoice:{invoice.id}">{invoice.number}</table-cell>
      <table-cell grow="1">{invoice.status}</table-cell>
      <table-cell grow="1" text-alignment="end">{invoice.amount}</table-cell>
    </table-row>
  </for>
</table>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically. The builder names differ from the markup: rows are `.data_row` and cells are `.data_cell` (the underlying widget kinds).

```zig
ui.el(.table, .{ .width = 420 }, .{
    ui.el(.data_row, .{}, .{
        ui.el(.data_cell, .{ .text = "Invoice", .grow = 1, .size = .sm, .style_tokens = .{ .foreground = .text_muted } }, .{}),
        ui.el(.data_cell, .{ .text = "Status", .grow = 1, .size = .sm, .style_tokens = .{ .foreground = .text_muted } }, .{}),
        ui.el(.data_cell, .{ .text = "Amount", .grow = 1, .size = .sm, .text_alignment = .end, .style_tokens = .{ .foreground = .text_muted } }, .{}),
    }),
    ui.el(.data_row, .{}, .{
        ui.el(.data_cell, .{ .text = "INV-001", .grow = 1, .on_press = .{ .open_invoice = 1 } }, .{}),
        ui.el(.data_cell, .{ .text = "Paid", .grow = 1 }, .{}),
        ui.el(.data_cell, .{ .text = "$250.00", .grow = 1, .text_alignment = .end }, .{}),
    }),
    ui.el(.data_row, .{ .selected = true }, .{
        ui.el(.data_cell, .{ .text = "INV-002", .grow = 1, .on_press = .{ .open_invoice = 2 } }, .{}),
        ui.el(.data_cell, .{ .text = "Pending", .grow = 1 }, .{}),
        ui.el(.data_cell, .{ .text = "$150.00", .grow = 1, .text_alignment = .end }, .{}),
    }),
})
```

Use [list](/docs/components/list) for icon rows or a single column; use a table when columns need to align.

## Attributes

| Attribute        | Description                                                                                                                                                                                                                                                                                   |
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `grow`           | Flex grow factor; give spacer one.                                                                                                                                                                                                                                                            |
| `selected`       | Selected state; often a \{a == b} equality.                                                                                                                                                                                                                                                   |
| `text-alignment` | Horizontal alignment of text content: start\|center\|end. Consumed by text (plain and wrapped), status-bar, and surface titles; controls that own their label placement (button, badge) ignore it. On a bubble's reactions child it names the pill's dock (end is the default trailing dock). |
| `on-press`       | Dispatch a Msg on press: tag or tag:\{payload}. Legal on any element — a bound press handler makes it pressable, and presses on plain text/icons inside it fall through to it (dragging still selects text).                                                                                  |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Tabs
url: "https://native-sdk.dev/docs/components/tabs"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `tabs` for a strip of choices with conditional content below it. Bind each child button’s `selected` state to the model and handle `on-press` to change the active tab. Render each panel through an `if`.

Markup lowers buttons inside `tabs` to segmented-control widgets. In Zig, use `segmented_control` children directly. Standalone segments are available through [segmented-control](/docs/components/segmented-control).

## Markup

```html
<column gap="16">
  <row>
    <tabs>
      <button selected="{tab == 'account'}" on-press="show_account">Account</button>
      <button selected="{tab == 'password'}" on-press="show_password">Password</button>
      <button selected="{tab == 'team'}" on-press="show_team">Team</button>
    </tabs>
    <spacer grow="1" />
  </row>
  <if test="{tab == 'account'}">
    <text foreground="text_muted">Manage your account details.</text>
  </if>
  <if test="{tab == 'password'}">
    <text foreground="text_muted">Change your password.</text>
  </if>
  <if test="{tab == 'team'}">
    <text foreground="text_muted">Manage your team.</text>
  </if>
</column>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically. In the builder the strip's children are `segmented_control` elements, the same widget kind available as a standalone markup leaf.

```zig
ui.el(.tabs, .{}, .{
    ui.el(.segmented_control, .{ .text = "Account", .selected = model.tab == .account, .on_press = .show_account }, .{}),
    ui.el(.segmented_control, .{ .text = "Password", .selected = model.tab == .password, .on_press = .show_password }, .{}),
    ui.el(.segmented_control, .{ .text = "Team", .selected = model.tab == .team, .on_press = .show_team }, .{}),
})
```

## Attributes

| Attribute  | Description                                                                                                                                                                                                  |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `selected` | Selected state; often a \{a == b} equality.                                                                                                                                                                  |
| `on-press` | Dispatch a Msg on press: tag or tag:\{payload}. Legal on any element — a bound press handler makes it pressable, and presses on plain text/icons inside it fall through to it (dragging still selects text). |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Textarea
url: "https://native-sdk.dev/docs/components/textarea"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `textarea` for multiline text entry. Bind `text` and `placeholder` to the model and handle `on-input` with the [input](/docs/components/input) edit reducer. Set `width` and `height`, or `grow`, to size it.

Enter and Shift+Enter insert newlines by default. With `on-submit`, Command+Enter on macOS or Ctrl+Enter elsewhere submits. Set `submit-on-enter="true"` for plain Enter to submit while Shift+Enter still inserts a newline. The primary modifier shortcut continues to submit.

Editing shortcuts follow [input](/docs/components/input#editing-keys). On macOS, Command+Backspace deletes the selection or text from the caret to the beginning of the logical line. Soft wrapping does not change that boundary.

## Markup

```html
<textarea width="320" height="96" placeholder="Write a release note" text="{draft}" on-input="draft_edited" />
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.el(.textarea, .{
    .width = 320,
    .height = 96,
    .placeholder = "Write a release note",
    .text = model.draft,
    .on_input = Ui.inputMsg(.draft_edited),
}, .{})
```

## Attributes

| Attribute         | Description                                                                                                                                                                                                                        |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`            | Text value for text-bearing elements; a literal or one \{binding}.                                                                                                                                                                 |
| `placeholder`     | Hint text shown while a text entry is empty.                                                                                                                                                                                       |
| `disabled`        | Disables the control; true/false or a \{binding}.                                                                                                                                                                                  |
| `autofocus`       | Focusable controls only: moves keyboard focus to the element when it mounts or when the value turns on (edge-triggered - holding it true never re-steals focus). The TEA way to focus an editor on create.                         |
| `submit-on-enter` | textarea only: true makes plain Enter dispatch on-submit while Shift+Enter inserts a newline; Cmd/Ctrl+Enter still submits. False or absent keeps the multiline default where Enter inserts and submission uses the primary chord. |
| `on-input`        | Names a Msg variant with canvas.TextInputEvent payload; delivers each text edit.                                                                                                                                                   |
| `on-submit`       | Dispatch a Msg on submit: tag or tag:\{payload}. Enter in a text field, primary+enter in a textarea; on a list-item, plain Enter dispatches it as the row's primary action while Space keeps the row's select (on-press).          |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Timeline
url: "https://native-sdk.dev/docs/components/timeline"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `timeline` for a vertical sequence of events. Each `timeline-item` has a title, optional description and metadata, and an indicator. `indicator` supplies badge text; `variant` selects its color. Set `connector="false"` on the last item to end the connecting line. Bind `on-press` to make an item actionable. Both elements can be ejected as markup templates. For horizontal stages, see [stepper](/docs/components/stepper).

## Markup

```html
<timeline gap="0">
  <timeline-item indicator="1" variant="primary" title="Build" description="Compiled 214 files in 1.8s." meta="zig build · 1.8s"></timeline-item>
  <timeline-item indicator="2" title="Test" description="Canvas and runtime suites passing." meta="zig build test · 42s" on-press="open_test_log"></timeline-item>
  <timeline-item indicator="3" title="Package" connector="false"></timeline-item>
</timeline>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically. The builder additionally takes an `icon` option that draws a registry icon inside the indicator badge (a vector check for finished steps).

```zig
ui.timeline(.{}, .{
    ui.timelineItem(.{ .icon = "check", .variant = .primary, .title = "Build", .description = "Compiled 214 files in 1.8s.", .meta = "zig build · 1.8s" }),
    ui.timelineItem(.{ .indicator = "2", .title = "Test", .description = "Canvas and runtime suites passing.", .meta = "zig build test · 42s", .on_press = .open_test_log }),
    ui.timelineItem(.{ .indicator = "3", .title = "Package", .connector = false }),
})
```

## Eject

Use the eject command to copy timeline into your project when you need to change its structure. SDK updates preserve your copy. The command refuses to overwrite an existing file.

`timeline` ejects as a markup template (`src/components/timeline.native`).

`timeline-item` ejects as a markup template (`src/components/timeline-item.native`).

```sh
native eject component timeline
native eject component timeline-item
```

The ownership model and what to do after ejecting are in [Use, eject, or build](/docs/building-components#use-eject-or-build).

## Attributes

| Attribute    | Description                                                                                                                                                        |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `gap`        | Spacing between items (plain number).                                                                                                                              |
| `grow`       | Flex grow factor.                                                                                                                                                  |
| `key`        | Sibling-scoped identity key.                                                                                                                                       |
| `global-key` | Parent-independent identity: ids survive reparenting between containers.                                                                                           |
| `label`      | Accessible name; when set it REPLACES the element's text as the announced name - screen readers and automation snapshots see the label, never the text it shadows. |

### Items

`title` is required on every item.

| Attribute     | Description                                                                                                                |
| ------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `title`       | timeline-item: bold first line (a literal or one \{binding}). Required.                                                    |
| `description` | timeline-item: wrapped muted preview under the title (a literal or one \{binding}).                                        |
| `meta`        | timeline-item: muted trailing meta line, e.g. "claude · sonnet · 1m 12s" (a literal or one \{binding}).                    |
| `indicator`   | timeline-item: leading badge text ("✓", a number); empty renders a small dot.                                              |
| `variant`     | timeline-item: indicator color variant (default\|primary\|secondary\|outline\|ghost\|destructive) - map run outcomes here. |
| `connector`   | timeline-item: false ends the connector rail (set on the last item).                                                       |
| `selected`    | timeline-item: selected state (true/false or a \{binding}).                                                                |
| `on-press`    | timeline-item: Msg dispatched from anywhere on the item (tag or tag:\{payload}); adds a trailing chevron.                  |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Toggle
url: "https://native-sdk.dev/docs/components/toggle"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `toggle` for a button that stays pressed. Bind `checked` and handle `on-toggle`. For grouped choices, place `toggle-button` elements in a `toggle-group`, bind each item’s `selected` state, and handle its `on-toggle`. The model determines whether the group allows one or several selections. Use [switch](/docs/components/switch) for a sliding on/off control.

## Markup

```html
<toggle-group>
  <toggle-button selected="{align == 'left'}" on-toggle="align_left">Left</toggle-button>
  <toggle-button selected="{align == 'center'}" on-toggle="align_center">Center</toggle-button>
  <toggle-button selected="{align == 'right'}" on-toggle="align_right">Right</toggle-button>
</toggle-group>
```

## Toggle

The standalone `toggle` element carries its own label; bind `checked` and handle `on-toggle` exactly like a [checkbox](/docs/components/checkbox).

```html
<row gap="12" cross="center">
  <toggle checked="{bold}" on-toggle="toggle_bold">Bold</toggle>
  <toggle checked="{italic}" on-toggle="toggle_italic">Italic</toggle>
  <toggle disabled="true" on-toggle="toggle_underline">Underline</toggle>
</row>
```

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.el(.toggle_group, .{}, .{
    ui.el(.toggle_button, .{ .text = "Left", .selected = model.align == .left, .on_toggle = .align_left }, .{}),
    ui.el(.toggle_button, .{ .text = "Center", .selected = model.align == .center, .on_toggle = .align_center }, .{}),
    ui.el(.toggle_button, .{ .text = "Right", .selected = model.align == .right, .on_toggle = .align_right }, .{}),
})
```

The standalone toggle:

```zig
ui.row(.{ .gap = 12, .cross = .center }, .{
    ui.el(.toggle, .{ .text = "Bold", .checked = model.bold, .on_toggle = .toggle_bold }, .{}),
    ui.el(.toggle, .{ .text = "Italic", .checked = model.italic, .on_toggle = .toggle_italic }, .{}),
    ui.el(.toggle, .{ .text = "Underline", .disabled = true, .on_toggle = .toggle_underline }, .{}),
})
```

## Attributes

| Attribute   | Description                                                                                                                          |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `text`      | Text value for text-bearing elements; a literal or one \{binding}.                                                                   |
| `checked`   | Checked state for checkbox/toggle; true/false or a \{binding}.                                                                       |
| `selected`  | Selected state; often a \{a == b} equality.                                                                                          |
| `disabled`  | Disables the control; true/false or a \{binding}.                                                                                    |
| `on-toggle` | Dispatch a Msg on toggle: tag or tag:\{payload}. Hit-target elements only (checkbox, toggle, toggle-button, switch, accordion, ...). |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Tooltip
url: "https://native-sdk.dev/docs/components/tooltip"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `tooltip` to annotate a control. It is a text leaf with model interpolation. Place it beside a trigger in a `stack` and set `anchor="above"` or `anchor="below"`. The runtime manages visibility on hover and keyboard focus; no model hover state is required. Without an anchor, the tooltip is ordinary static content. For a chat-message surface, see [bubble](/docs/components/bubble).

## Markup

```html
<stack>
  <button variant="outline" on-press="toggle_bold">Bold</button>
  <tooltip anchor="above">Bold the selection</tooltip>
</stack>
```

## Hover intent

An anchored tooltip appears after `tooltip-delay` milliseconds of pointer hover (default 600). Set the delay to `0` for immediate display. After the pointer leaves a shown tooltip, a shared 400 ms window allows the next trigger’s tooltip to appear immediately. Theme metric tokens `tooltip_show_delay_ms` and `tooltip_warm_window_ms` control these defaults.

The tooltip stays open while the pointer is over the trigger, the tooltip, or the gap between them. Scrolling updates hit testing: moving a trigger out from under the pointer hides or cancels its tooltip; bringing one under the pointer starts the delay.

Keyboard focus shows the tooltip immediately. Moving focus away or pressing Escape hides it without opening the pointer warm window. Any pointer-down or Space/Enter activation cancels pending display, hides the tooltip, and clears that window. Losing view or window focus clears tooltip state.

Without `anchor`, the element displays whenever it is present in the view. Use a model-bound `if` to control that form.

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.el(.stack, .{}, .{
    ui.button(.{ .variant = .outline, .on_press = .toggle_bold }, "Bold"),
    ui.el(.tooltip, .{ .text = "Bold the selection", .anchor = .above }, .{}),
})
```

## Attributes

| Attribute          | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`             | Text value for text-bearing elements; a literal or one \{binding}.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `anchor`           | dropdown-menu and tooltip: floats the surface against its PARENT's frame instead of the flow (literal below or above; either side auto-flips at the window edges). Late z-pass above the whole tree, window-clipped — never cropped by a scroll pane, never reflows siblings. Put the surface beside its trigger inside a stack. An anchored tooltip's visibility is runtime-owned (hover intent on the trigger; see tooltip-delay), unlike the model-owned dropdown.                                                                                                              |
| `anchor-alignment` | dropdown-menu and tooltip (with anchor): horizontal alignment against the anchor - start, end, or stretch (stretch also widens the surface to at least the anchor's width, the select-menu look).                                                                                                                                                                                                                                                                                                                                                                                  |
| `anchor-offset`    | dropdown-menu and tooltip (with anchor): literal gap in points between the anchor edge and the surface (default 4).                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `tooltip-delay`    | tooltip only, beside anchor: hover-intent show delay in milliseconds (a plain number or one \{binding}) - the runtime shows the anchored tooltip after its trigger has been hovered this long on the recorded frame clock. 0 shows the instant the trigger is hovered; absent follows the 600ms token default. A shared 400ms warm window after a pointer-hovered tooltip hides on leave skips the delay on the next trigger (no other hide cause warms); keyboard-focus reveals are always immediate. A delay without an anchor is a teaching error (it would be silently inert). |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Tree
url: "https://native-sdk.dev/docs/components/tree"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `tree` for a hierarchy with expandable rows. Rows are ordinary elements, usually [list items](/docs/components/list), with `role="treeitem"`. Bind `expanded` and handle `on-toggle` for disclosure, and conditionally render the children. The model owns expansion and selection.

Up/Down move through visible rows, Left collapses or moves to the parent, Right expands or moves to the first child, and Home/End move to the first or last row. With `on-change`, selection and pointer activation can have separate handlers; without it, selection uses `on-press`.

## Markup

```html
<tree width="340" gap="2">
  <list-item icon="folder-open" role="treeitem" tree-level="1" expanded="{src_open}" on-toggle="toggle_src" on-press="select_src" on-change="select_src">src</list-item>
  <if test="{src_open}">
    <row>
      <column width="20" />
      <column gap="2" grow="1">
        <list-item icon="file-text" role="treeitem" tree-level="2" selected="{file == 'main'}" on-press="preview_main" on-change="select_main">main.zig</list-item>
        <list-item icon="file-text" role="treeitem" tree-level="2" selected="{file == 'view'}" on-press="preview_view" on-change="select_view">view.zig</list-item>
      </column>
    </row>
  </if>
  <list-item icon="folder" role="treeitem" tree-level="1" expanded="{assets_open}" on-toggle="toggle_assets" on-press="select_assets" on-change="select_assets">assets</list-item>
</tree>
```

The indent is plain layout — a fixed-width spacer column beside the children. Because those indented rows are layout siblings rather than widget descendants of the `src` row, `tree-level` supplies their one-based logical hierarchy. Structurally nested tree rows can omit it. Omit `expanded` on leaf rows; only rows that bind it participate in Left/Right disclosure.

## Programmatic construction (Zig)

In a Zig view, the `canvas.Ui` builder constructs the same tree programmatically:

```zig
ui.tree(.{ .width = 340, .gap = 2 }, .{
    ui.listItem(.{
        .icon = "folder-open",
        .tree_level = 1,
        .expanded = model.src_open,
        .on_toggle = .toggle_src,
        .on_press = .select_src,
        .on_change = .select_src,
        .semantics = .{ .role = .treeitem },
    }, "src"),
    if (model.src_open) ui.row(.{}, .{
        ui.column(.{ .width = 20 }, .{}),
        ui.column(.{ .gap = 2, .grow = 1 }, .{
            ui.listItem(.{ .icon = "file-text", .tree_level = 2, .selected = model.file == .main, .on_press = .preview_main, .on_change = .select_main, .semantics = .{ .role = .treeitem } }, "main.zig"),
            ui.listItem(.{ .icon = "file-text", .tree_level = 2, .selected = model.file == .view, .on_press = .preview_view, .on_change = .select_view, .semantics = .{ .role = .treeitem } }, "view.zig"),
        }),
    }) else ui.stack(.{}, .{}),
    ui.listItem(.{
        .icon = "folder",
        .tree_level = 1,
        .expanded = model.assets_open,
        .on_toggle = .toggle_assets,
        .on_press = .select_assets,
        .on_change = .select_assets,
        .semantics = .{ .role = .treeitem },
    }, "assets"),
})
```

## Attributes

| Attribute    | Description                                                                                                                                                                                                                  |
| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `role`       | Accessibility role (listitem, treeitem, button, ...). treeitem also makes the row part of its tree's roving keyboard focus set.                                                                                              |
| `tree-level` | Flat sibling rows with role="treeitem": one-based logical depth used by Left/Right to resolve parents and first children. Omit it when tree rows are structurally nested.                                                    |
| `expanded`   | Tree rows (role="treeitem"): disclosure state (true/false or a \{binding}). Omit on leaves; expanded rows collapse on Left, collapsed ones expand on Right, both through on-toggle - the model owns the state.               |
| `selected`   | Selected state; often a \{a == b} equality.                                                                                                                                                                                  |
| `on-toggle`  | Dispatch a Msg on toggle: tag or tag:\{payload}. Hit-target elements only (checkbox, toggle, toggle-button, switch, accordion, ...).                                                                                         |
| `on-change`  | Dispatch a Msg on change: tag or tag:\{payload}. Hit-target elements only. On a treeitem, Up/Down/Left/Right/Home/End focus movement prefers on-change over on-press, separating keyboard selection from pointer activation. |
| `on-press`   | Dispatch a Msg on press: tag or tag:\{payload}. Legal on any element — a bound press handler makes it pressable, and presses on plain text/icons inside it fall through to it (dragging still selects text).                 |
| `gap`        | Spacing between children (plain number). Rejected on stacking containers (stack, panel, card, the modal surfaces) — they layer children; put a column (or row) inside for flow.                                              |
| `label`      | Accessible name; when set it REPLACES the element's text as the announced name - screen readers and automation snapshots see the label, never the text it shadows.                                                           |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Video
url: "https://native-sdk.dev/docs/components/video"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use `video` to display the app’s single platform-decoded video. macOS uses AVFoundation; Windows and Linux currently report no decoder capability and dispatch a `failed` event when loading.

`src` accepts an app-assets path or an HTTP(S) URL and resolves local assets first. Changing it replaces playback. `autoplay` is enabled by default; `loop` and `muted` configure new playback. The picture preserves its aspect ratio and fits inside the box with black bars as needed.

Set `controls` to include play/pause, seeking, and time readouts without model-owned transport state. For custom controls, use `Cmd.videoLoad` and `Cmd.videoCtl` in TypeScript, or the Zig video effects.

Before the first frame, the surface displays an id-derived placeholder. Reference screenshots and replay use the same placeholder. Transport events are journaled; decoded pixels remain outside the model and replay data.

## Markup

```html
<column gap="10">
  <video src="assets/clips/launch.mp4" controls height="200" label="Launch clip" />
  <video src="{model.trailerUrl}" autoplay="false" muted height="200" label="Trailer" />
</column>
```

The element is a leaf (no children) with no intrinsic size — give it definite `width`/`height` or flex `grow`, and a `label` for screen readers (the image rule).

## Programmatic construction (Zig)

```zig
ui.video(.{
    .src = "assets/clips/launch.mp4",
    .controls = true,
    .height = 200,
    .label = "Launch clip",
})
```

## Custom controls

Bare `<video src>` plus your own chrome is the audio pattern end to end: transport events arrive as ordinary messages, and the transport verbs drive the same single player the element loaded.

```zig
// update
.toggle_play => if (model.playing) fx.pauseVideo() else fx.playVideo(),
.scrubbed => |fraction| fx.seekVideo(@intFromFloat(fraction * @as(f64, @floatFromInt(model.duration_ms)))),
```

See the video-player example for a complete player at both tiers — house chrome on one screen, custom controls on another.

## Attributes

| Attribute    | Description                                                                                                                                                                                                                                                                                                                                                   |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `src`        | video: the source string — an app-assets path (tried first as a local file) or an http(s) URL (streamed progressively when the local file is absent), a literal or one \{binding}. Declaring it loads the app's SINGLE video player (a changed src replaces the playback whole); omit it to render the surface alone over a playback your update code drives. |
| `controls`   | video: composes the house transport chrome under the picture — play/pause, a scrub slider, and elapsed/duration readouts, runtime-driven so the model carries no transport state. Omit it for the bare surface and compose your own controls from the video command vocabulary.                                                                               |
| `autoplay`   | video: start playing as soon as the load lands (the default). false loads paused at the first frame — the poster shape; play resumes it.                                                                                                                                                                                                                      |
| `loop`       | video: wrap from the natural end back to the start and keep playing. A looping playback never delivers a completed event — it never ends.                                                                                                                                                                                                                     |
| `muted`      | video: start with the audio track muted (a reversible switch, independent of the remembered volume).                                                                                                                                                                                                                                                          |
| `width`      | Definite width (plain number): the element is exactly this wide (no intrinsic size).                                                                                                                                                                                                                                                                          |
| `height`     | Definite height (plain number): the element is exactly this tall (no intrinsic size).                                                                                                                                                                                                                                                                         |
| `grow`       | Flex grow factor.                                                                                                                                                                                                                                                                                                                                             |
| `label`      | Accessible name for the picture (the image rule: unnamed video degrades to an unnamed image for screen readers; label="" marks it decorative).                                                                                                                                                                                                                |
| `key`        | Sibling-scoped identity key.                                                                                                                                                                                                                                                                                                                                  |
| `global-key` | Parent-independent identity: ids survive reparenting between containers.                                                                                                                                                                                                                                                                                      |

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Virtual List
url: "https://native-sdk.dev/docs/components/virtual-list"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Use the Zig builder’s virtual list to build only visible rows and overscan while representing the full dataset in a scroll region. Call `ui.virtualWindow` for the visible index range, build keyed rows for that range, and pass them to `ui.virtualList`. The runtime calculates the window from retained scroll state, viewport height, and fixed or estimated row extents; the model owns the dataset.

## Markup

Windowed construction is available only through the Zig builder. Markup cannot bind a runtime window request or an extent-estimate function. A markup [virtualized list](/docs/components/list) builds all authored rows and limits layout and painting to the visible window. Markup scroll regions can use `on-reach-end` to request more data.

## Programmatic construction (Zig)

Declare the list options once and use them twice — `virtualWindow` resolves which items are visible, `virtualList` mounts exactly those rows. Keys carry per-item identity: a row that scrolls away and back returns under the same structural id, so engine-owned and model-owned row state survive window shifts.

```zig
const options = Ui.VirtualListOptions{
    .id = "timeline",              // stable identity (global key + scroll-state lookup)
    .item_count = model.loaded,    // the MODEL owns the data; this is all the runtime sees
    .item_extent = 84,             // fixed row height: the uniform fast path
    .overscan = 4,                 // rows built beyond the visible range on each side
    .grow = 1,
    .on_reach_end = .load_more,    // the infinite-scroll fetch signal
};
const window = ui.virtualWindow(options);
const rows = ui.arena.alloc(Ui.Node, window.itemCount()) catch return ...;
for (rows, 0..) |*row, offset| {
    const index = window.start_index + offset;
    var node = postRow(ui, model, index); // read model data for ONE item
    node.key = .{ .int = @intCast(index) };
    row.* = node;
}
return ui.virtualList(options, window, .{rows});
```

No `on_scroll` binding is required: the runtime owns the offset (wheel, kinetic, keyboard, and the native scroll driver all land on it), and every scroll observation re-derives the view so the window follows. Bind `on_scroll` only when the model wants to observe the position.

## The data-window seam

`ui.virtualWindow` is a value the view **reads during build** — the same direction of flow as the appearance and window-chrome channels, never a callback into the model. Under `UiApp` the window source resolves the list's retained scroll state by its declared `id`; on the first build (no state yet) it assumes offset 0 at the canvas height, and if the freshly laid-out geometry proves the window under-covered (first build, a window resize), the view is derived once more against it — the window converges within the same rebuild. In bare tree builds (tests, previews) with no source installed, `viewport_fallback` stands in.

## Reach-end

`on_reach_end` is the infinite-fetch signal for any scroll container (markup: `on-reach-end` on `scroll`): dispatched when a user scroll comes within one viewport of the content end, with hysteresis — it fires once per approach and re-arms only after the offset retreats past one and a half viewports, which appending a batch causes on its own by growing the extent. A programmatic jump to the end fires once and never re-arms while the offset stays near the end — re-arming needs a post-scroll observation at least 1.5 viewports from it. `update` appends the next batch; nothing else is required.

| Attribute      | Description                                                                                                                                                                                                                                                                                                                             |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `on-reach-end` | scroll element only: Msg (tag or tag:\{payload}) dispatched when a user scroll comes within one viewport of the content end - the infinite-scroll fetch signal. Fires once per approach with hysteresis: it re-arms only after the offset retreats past 1.5 viewports, which appending a batch causes on its own by growing the extent. |

## Options

`Ui.VirtualListOptions` fields (shared by `virtualWindow` and `virtualList`):

<table>
  <thead>
    <tr>
      <th>
        Option
      </th>

      <th>
        Description
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `id`
      </td>

      <td>
        Stable list identity: becomes the element's

        `global_key`

        and the key the runtime resolves scroll state by. Unique per tree.
      </td>
    </tr>

    <tr>
      <td>
        `item_count`
      </td>

      <td>
        Total items currently in the model. Content extent, scrollbar, and

        `list_item_index/count`

        semantics derive from it.
      </td>
    </tr>

    <tr>
      <td>
        `item_extent`
      </td>

      <td>
        Fixed per-item extent in points. The runtime calculates the range and total extent without measuring every row. With

        `extent_estimate`

        set it becomes the fallback estimate only (may be 0).
      </td>
    </tr>

    <tr>
      <td>
        `gap`
      </td>

      <td>
        Vertical gap between rows (part of the row stride).
      </td>
    </tr>

    <tr>
      <td>
        `overscan`
      </td>

      <td>
        Rows built beyond the visible range on each side (default 4): scroll slack until the next window rebuild lands.
      </td>
    </tr>

    <tr>
      <td>
        `viewport_fallback`
      </td>

      <td>
        Viewport height assumed when no runtime scroll state exists (bare builds); under

        `UiApp`

        the installed window source falls back to the canvas height instead.
      </td>
    </tr>

    <tr>
      <td>
        `width`

        /

        `height`

        /

        `min_width`

        /

        `grow`

        /

        `padding`
      </td>

      <td>
        The container's box, as on any element.
      </td>
    </tr>

    <tr>
      <td>
        `style`

        /

        `style_tokens`

        /

        `semantics`
      </td>

      <td>
        Container styling and semantics; the role defaults to

        `list`

        .
      </td>
    </tr>

    <tr>
      <td>
        `on_scroll`
      </td>

      <td>
        Optional scroll observation (

        `Ui.scrollMsg`

        constructor). Not required for windowing.
      </td>
    </tr>

    <tr>
      <td>
        `on_reach_end`
      </td>

      <td>
        The approach-end Msg (see above).
      </td>
    </tr>

    <tr>
      <td>
        `extent_estimate`

        /

        `extent_context`
      </td>

      <td>
        Variable-extent mode: a pure estimate function (

        `fn (context, logical_index) f32`

        ) from model facts — line counts, byte counts — never from layout. Rows lay out at their intrinsic heights; the engine measures mounted rows and converges the scroll geometry, anchored so corrections never move visible content.
      </td>
    </tr>

    <tr>
      <td>
        `index_base`
      </td>

      <td>
        Logical index of item 0 (prepend-stable identity). Decrease it by the prepended count to load older history: row identity, measured extents, and the viewport anchor all survive.
      </td>
    </tr>

    <tr>
      <td>
        `anchor`
      </td>

      <td>
        `.leading`

        (default) or

        `.trailing`

        — the chat contract: open at the bottom, stay pinned through appends while the user sits at the bottom, never yank a scrolled-away viewport.
      </td>
    </tr>

    <tr>
      <td>
        `on_reach_start`
      </td>

      <td>
        The approach-START Msg (load older history), with the same hysteresis as reach-end; a prepend re-arms it by growing the offset.
      </td>
    </tr>

    <tr>
      <td>
        `overscroll`
      </td>

      <td>
        Edge behavior of the list's scroll region.

        `.default`

        follows the

        `ScrollPhysics.overscroll`

        token — off unless a theme flips it, so scrolling pins at the content edges;

        `.rubber_band`

        lets this list bounce past them;

        `.none`

        pins it regardless of the token.
      </td>
    </tr>
  </tbody>
</table>

## Variable-extent rows

Chat transcripts, mixed-height feeds, and markdown-bearing lists set `extent_estimate` instead of a fixed `item_extent`. The estimate supplies extents for unmounted rows; mounted rows lay out at their real wrapped heights and the engine records those measurements into a budgeted per-list offset table, so the total extent — the scrollbar, the scroll semantics, the native driver's content size — converges to truth as the user visits the list. Account for two behaviors:

- **The scrollbar may drift as estimates correct.** The correction store has a fixed budget. Evicted rows use their estimates until they are revisited.
- **Visible content never jumps.** The built window is anchored on the first visible row: it lands exactly at the offset table's edge for it, freshly mounted rows stack above/below at their real heights, and every correction shifts the scroll offset and the geometry together. Estimate error surfaces off screen, not under the pointer.

`anchor = .trailing`, `on_reach_start`, and `index_base` complete the transcript shape — see `examples/feed` for the mixed-height showcase and the options table above for each field.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Testing in CI
url: "https://native-sdk.dev/docs/testing/ci"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

`native init --full` and web-frontend scaffolds generate `.github/workflows/ci.yml`. The default scaffold does not include CI. Use the recipes below to add a workflow or adapt the generated one.

## Three tiers of confidence

Native SDK apps test in three tiers, each running in progressively more realistic environments:

1. **Logic tests, everywhere** — `native test -Dplatform=null` runs the app’s `Model`/`Msg`/`update` loop, the markup compiler, and the canvas layout engine on the headless null platform. No window server, no GPU, no OS dependencies: these run on any runner (and in the generated workflow's `test` job).
2. **Xvfb automation smoke, on Linux** — the app builds with `-Dplatform=linux -Dautomation=true`, launches a real GTK window under a virtual X display, and is driven through the file-based [automation](/docs/automation) protocol: wait for readiness, assert on the accessibility snapshot, capture a screenshot. This proves the real render path (window, GPU surface, widget semantics) without GUI hardware.
3. **Real windows, on macOS runners** — GitHub's macOS runners provide a GUI-capable session, so AppKit windows actually open there. The same `native automate` commands drive them; the framework's own CI runs its WebView and GPU smoke suites this way.

Most apps get lasting value from tiers 1 and 2 alone — that is what the generated workflow sets up.

## Deterministic screenshots as goldens

`native automate screenshot <view-label>` renders a `gpu_surface` view through the deterministic CPU reference renderer at scale 1. Two captures of an unchanged scene are byte-identical on the same machine, so screenshot diffs can serve as golden checks: commit a known-good PNG and `cmp` against a fresh capture in CI, or just assert the artifact is non-empty as the generated workflow does. Text measurement can differ across OS versions, so compare goldens captured on the same runner image rather than across platforms.

## Snapshot assertions with `automate assert`

`native automate assert` replaces grep-and-sleep chains with one polling command:

```bash
native automate assert 'gpu_nonblank=true' 'role=button name="Reset"' 'total: 0'
```

Each argument is a regex that must match `snapshot.txt`. The command polls until every pattern matches or the timeout expires, then exits 0 (CI-friendly). On failure it exits non-zero, names each missing pattern, and prints the snapshot tail so the CI log carries the evidence.

- `--timeout-ms <n>` — how long to keep polling (default 30000).
- `--absent` — invert the check: every pattern must be gone from the snapshot. Useful for `'error event='` style regressions or waiting for a widget to disappear.

The supported regex subset is grep-like: literals, `.`, postfix `*` `+` `?`, line anchors `^` and `$`, character classes `[a-z]` / `[^0-9]`, and `\d` `\w` `\s` escapes (no groups or alternation — pass multiple patterns instead).

```bash
# Wait for a state transition, then prove nothing degraded.
native automate assert --timeout-ms 10000 '4 open'
native automate assert --absent 'error event=' 'dispatch_errors=[1-9]'
```

## The generated workflow

`native init --full` writes two jobs for native apps (web-frontend scaffolds get the `test` job only, since the smoke recipe is native-specific):

```yaml
env:
  # build.zig.zon expects the Native SDK framework checkout at this
  # path, relative to the repository root.
  NATIVE_SDK_PATH: "../native-sdk"
```

Both jobs fetch the framework into that path when it is missing, so the app's `build.zig.zon` path dependency resolves on a fresh runner. If your framework checkout lives elsewhere (or you scaffolded from an unusual directory), adjust `NATIVE_SDK_PATH` and `build.zig.zon` together.

The `test` job is tier 1:

```yaml
- uses: vercel-labs/setup-zig@v1
  with:
    version: 0.16.0
- run: zig build test -Dplatform=null
```

The `smoke` job is tier 2. It installs GTK and Xvfb, builds the framework CLI, then builds and drives the app headless:

```yaml
- name: Build and drive the app headless
  run: |
    set -euo pipefail
    cli="$NATIVE_SDK_PATH/zig-out/bin/native"
    zig build -Dplatform=linux -Dweb-engine=system -Dautomation=true
    rm -rf .zig-cache/native-sdk-automation
    xvfb-run -a ./zig-out/bin/my-app &
    pid=$!
    trap 'kill "$pid" >/dev/null 2>&1 || true' EXIT
    "$cli" automate wait
    "$cli" automate assert 'gpu_nonblank=true' 'role=button name="Reset"' 'total: 0'
    "$cli" automate screenshot main-canvas
    test -s .zig-cache/native-sdk-automation/screenshot-main-canvas.png
```

Step by step:

1. `rm -rf .zig-cache/native-sdk-automation` clears stale automation artifacts so assertions never pass against a previous run.
2. `xvfb-run -a` launches the real binary under a virtual display; the `trap` guarantees the app dies with the job.
3. `automate wait` blocks until the runtime publishes `ready=true`.
4. `automate assert` proves the GPU surface presented non-blank pixels and the scaffolded counter UI (the Reset button and the `total: 0` status bar) reached the accessibility snapshot.
5. `automate screenshot` renders the `main-canvas` view to a PNG and the final `test -s` fails the job if it is empty.

As the app grows, extend the assert line with patterns for your own widgets, add `automate widget-click` interactions between assertions, or check screenshots against committed goldens.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Testing
url: "https://native-sdk.dev/docs/testing"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Native SDK apps test headlessly by default: the view is a function of the model, dispatch is typed, and the layout engine runs without a window server. Unit tests drive the real loop, `TestHarness` covers runtime integration, and the [automation](/docs/automation) harness drives the live app. For running these tiers on GitHub Actions — including the workflow `native init --full` scaffolds — see [Testing in CI](/docs/testing/ci).

## TypeScript tests against compiled apps

For an app with `src/core.ts`, add `tests/*.test.ts` and run `native test`. The build compiles the app with scriptc and links a headless host around the existing `TestHarness`. Node.js 24 runs the test code; the app's model, updates, markup, layout, effects, and automation execute in the native process.

```ts
// tests/counter.test.ts
import assert from "node:assert/strict";
import test from "node:test";
import { NativeApp, findWidget } from "@native-sdk/core/testing";

test("increment and replay", async () => {
  const app = await NativeApp.start({ wallMs: 77000 });
  try {
    let snapshot = await app.snapshot();
    snapshot = await app.click(findWidget(snapshot, {
      role: "button", name: "+",
    }));
    assert.equal(snapshot.model.count, 1);
    assert.ok(snapshot.widgets.some(widget => widget.name.includes("total: 1")));

    const replay = await app.verifyReplay();
    assert.deepEqual(replay.snapshot.model, snapshot.model);
  } finally {
    await app.close();
  }
});
```

`findWidget` requires exactly one matching accessibility role and name. Add `view` to distinguish identical controls in different views. Use a fresh snapshot after an interaction. Widget identities and effect keys are decimal strings so JavaScript preserves all 64 bits. Scroll regions also expose `widget.scroll`, the applied two-axis `ScrollState` with offsets, velocities, and viewport/content extents widened exactly from native f32. Other widgets have `scroll: null`.

Each `NativeApp.start()` creates an independent native process. `click`, `action`, `menu`, and `frame` return the resulting snapshot. Actions support `focus`, `press`, `toggle`, `increment`, `decrement`, and `dismiss`. Requests are serialized, time out after 10 seconds by default (`timeoutMs` overrides this), and reject on native errors or process exits. Always close the app in `finally`, or use `await using`.

For text-entry widgets, `setText(widget, text)` replaces the content through native select-all and text input, preserving leading and trailing whitespace. `selectText(widget, anchor, focus)` uses UTF-8 byte offsets. `composeText`, `commitComposition`, and `cancelComposition` drive native IME events. Each method returns a snapshot and records the interaction for replay. `key(view, "cmd+a")`, `key(view, "backspace")`, and `key(view, "enter")` use the native keyboard path.

For drag and scroll tests, `drag(widget, { x, y })` sends a physical pointer gesture through native hit testing. Coordinates are in the receiving canvas. `beginDrag` leaves the gesture active so you can inspect its preview; finish it with `pointer(widget, "up", point)` or cancel with `key(widget.view, "escape")`. `pointer` also accepts `"down"`, `"drag"`, and `"cancel"`, with an optional `{ x, y }` movement delta. `wheel(widget, deltaY, deltaX)` scrolls at the widget's center. `dropFiles(view, paths, window)` sends a platform file-drop event; an empty view addresses the window, and the default window is `1`. These inputs use the normal runtime and replay journal.

For secondary windows, `snapshot.windows` lists live window identities, labels, titles, bounds, focus, and hidden state. `frame()` presents all live canvases. Select controls with `findWidget(snapshot, { role, name, view })`; pointer, text, and keyboard operations route to that canvas. `closeWindow(window)` sends a native user-close notification, including the window's close command. `menu(command, windowId)` addresses a specific window and defaults to the primary window.

`contextPress(widget)` presents the declared native menu. `snapshot.contextMenu` contains the exact target, correlation token, pointer position, and all item records; labels remain lossless byte arrays. Pass that record to `contextMenuAction(menu, itemId)` to select an item, or use item `0` to dismiss. A superseded record exercises the native stale-token path and cannot select a successor menu. These events replay through the normal runtime.

`snapshot.webViews` lists every open WebView's URL, bounds, layer, transparency, bridge setting, and zoom. The test host enforces the app manifest's permissions and navigation origins during startup and replay, so undeclared navigation fails as it does in a shipping app.

### Models, effects, and replay

`snapshot.model` is the committed native model's JSON projection: byte values become number arrays, enums become strings, and tagged unions use the generated mirror's `{arm: payload}` shape. Its TypeScript type is a JSON record. `snapshot.widgets` contains accessibility state, bounds, and available actions. Each widget's `value` reports its applied numeric accessibility value, or `null` when it has none; use it to assert slider and split-divider positions, including retained values outside the model. The `fingerprint` covers the native accessibility tree.

The host drives the primary canvas and every live secondary canvas on the current desktop build target. It uses fake effects and a fixed clock (`wallMs`, default zero). It does not start production services, load persistence, read launch environment values, or open app-data directories.

Inspect `snapshot.effects.requests`, then answer a request with `app.respond(key, bytes, ok)`. For typed services, successful responses use the service contract's canonical binary encoding; the generated result decoder routes them to the typed Msg arm. Failed responses carry error bytes. See `examples/service-feed-reader/tests/view.test.ts` for a record response fixture. Inspect `snapshot.effects.timers`, then call `app.fireTimer(key)` to send a journaled platform timer event. `snapshot.effects.recorded` counts journaled effect results, including clock reads.

Rendering uses `NullPlatform`; OS windows, GPU presentation, and platform integrations still need live [automation tests](/docs/automation).

`verifyReplay()` finishes the in-memory recording, creates a fresh native runtime, and verifies recorded checkpoints, the final accessibility fingerprint, and the final model. Its report includes event, effect, and checkpoint counts plus the replayed snapshot. It is terminal: afterward, use only `snapshot()` and `close()`. The journal and each response are capped at 8 MiB; each request is capped at 64 KiB. Exceeding a bound fails the test.

Tests are discovered directly under `tests/` (no recursive discovery), run in a shared Node process, and resolve the SDK selected by the build. The core remains compiled even when an app has no local `node_modules`. For editor checking of Node test files, install `@types/node` and use a separate test tsconfig with `types: ["node"]` and `include: ["tests/**/*.ts"]`; keep the deterministic core's tsconfig separate.

## Zig full-loop UI tests

For apps created with `--template zig-core`, the generated `src/tests.zig` shows the core pattern: build the real view from the markup, find a widget, dispatch its press exactly like the runtime would, and assert on the model and the rebuilt view:

```zig
var view = try canvas.MarkupView(Model, Msg).init(arena, main.app_markup);
var ui = canvas.Ui(Msg).init(arena);
const tree = try ui.finalize(try view.build(&ui, &model));

var fx = main.Effects.init(testing.allocator);           // fake-executor effects channel
defer fx.deinit();
fx.executor = .fake;

const plus = findByText(tree.root, .button, "+").?;           // walk tree.root
main.update(&model, tree.msgForPointer(plus.id, .up).?, &fx); // dispatch like the runtime
try testing.expectEqual(@as(i64, 1), model.count);
```

Two `msgForPointer` traps: a disabled control yields `null` (assert `== null` rather than unwrapping when testing disabled states), and the tree is a snapshot — after each dispatch, rebuild the view before pressing anything again. Widget ids are stable across rebuilds, so asserting an id stayed constant while its text changed is the standard way to prove keyed identity. Sliders have their own split: `msgForValue(id, value)` fires only a Zig builder's `on_value` constructor and returns `null` for a markup slider — a markup slider binds a plain `on-change`, so assert its dispatch with `msgFor(id, .change)`.

The same tests can push the tree through the layout engine (`canvas.layoutWidgetTree`) to assert real frames — see the generated `src/tests.zig` for both patterns. Effects-using apps swap in the fake executor (`app_state.effects.executor = .fake`) to assert on spawn/fetch/file requests and feed results back deterministically — see [Native UI: Effects](/docs/native-ui#effects).

## TestHarness

`TestHarness` provides a headless test driver using `NullPlatform` and a `BufferSink` for capturing trace records:

```zig
const harness = try native_sdk.TestHarness().create(std.testing.allocator, .{});
defer harness.destroy(std.testing.allocator);
```

The harness embeds the multi-megabyte `Runtime`, so create it on the heap (`create`/`destroy`) — stack instances overflow test threads, and the same rule applies to `UiApp` instances in tests (`App.create`/`destroy`). The harness provides a pre-configured runtime with `NullPlatform` and a trace sink that captures records in memory; handler and update errors propagate (fail the test) instead of degrading. Use it to test runtime integration: lifecycle events, command dispatch, and full `UiApp` frames.

`TestHarness` is the same mechanism used by the framework's own test suite to verify bridge policy enforcement, window management, and lifecycle correctness.

## Headless tests

The default test suite does not require a window server:

```bash
zig build test
zig build test-desktop
zig build test-automation-protocol
zig build test-platform-info
zig build test-examples
zig build test-examples-native
zig build test-examples-mobile
```

Bridge and IPC coverage lives in the headless desktop tests: they inject platform bridge events, exercise command policy and handlers, and assert the platform response without launching a WebView. Automation command parsing is covered by `test-automation-protocol`.

`test-examples` runs all repository example checks. Use the narrower example groups when you want a faster pass over one slice.

`test-examples-native` runs every native-first example's headless suite with `-Dplatform=null` from the repository root. Zero-config examples (app.zon + src, no build files) are driven through the `native` CLI's generated build graph — the same path `native test` takes in any app directory — while examples that own a `build.zig` run their in-directory `zig build test`.

`test-examples-mobile` verifies the iOS, Android, and shared `mobile-shell` example layouts so the embedded host scaffolds stay present. It also checks the shared mobile-shell platform, capability, and command metadata.

## WebView smoke tests

WebView smoke coverage is a separate macOS integration step using [Automation](/docs/automation):

```bash
zig build test-webview-smoke -Dplatform=macos
zig build test-native-shell-smoke -Dplatform=macos
zig build test-webview-cef-smoke -Dplatform=macos -Dweb-engine=chromium
```

This step:

1. Starts the system WebView example with automation and the JS bridge enabled
2. Waits for a published automation snapshot (`native automate wait`)
3. Verifies main window, source, and native/WebView metadata (`native automate snapshot`)
4. Sends a `native.ping` request through `native automate bridge`
5. Verifies child WebView create, resize, navigate, and close commands
6. Verifies the responses

The CEF smoke step additionally requires a local CEF layout or `-Dcef-auto-install=true`; it exercises `native.ping` and child WebView create/resize/navigate/close through the automation bridge. These steps are intentionally opt-in because they need a GUI-capable macOS session.

The native-shell smoke step launches `examples/native-shell`, verifies toolbar/sidebar/statusbar/WebView rows in the automation snapshot, drives native focus traversal, drives a main-window resize and checks relayout bounds, dispatches `app.refresh` through bridge, menu, toolbar, and shortcut command paths, creates and closes a child preview WebView, and checks that the native status label reflects each command source.

## NullPlatform

`NullPlatform` is a headless platform stub that records loaded sources and dispatched events without creating real windows. Use it in tests and with `EmbeddedApp`:

```zig
var null_platform = native_sdk.NullPlatform.init(.{});
var runtime = native_sdk.Runtime.init(.{
    .platform = null_platform.platform(),
});
```

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: TypeScript Cores
url: "https://native-sdk.dev/docs/typescript"
docs_index: /llms.txt
lastUpdated: 2026-10-19
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

An app core defines `Model` (app state), `Msg` (typed messages), `update(model, msg)` (state transitions), and pure helpers. The default core is `src/core.ts`. The `@native-sdk/core` frontend checks its supported TypeScript subset, and the external core compiler produces native code. Unsupported operations produce diagnostics before the app builds.

The default project uses `src/core.ts`, `src/app.native`, and `app.json`; existing `app.zon` manifests remain supported. Put filesystem access, JSON processing, regexes, collections, or child-process work in [TypeScript services](/docs/typescript/services) under `src/services/`. Services compile to native code and return results through messages. Zig cores use the same runtime loop. Toolkit extensions, including custom widgets and render passes, are implemented in Zig.

The same core can also run under Node.js. Use `native dev --core` to exercise messages, effects, and subscriptions without rebuilding or opening a window:

```bash
native dev --core   # run the core under node's virtual host: dispatch Msgs as
                    # JSON lines, watch the model + effect transcript
native dev          # build and run the real app (markup hot reload)
native check        # subset-check core.ts + validate markup + app.json
native build        # ReleaseFast binary; native test runs the app's tests
```

## The contract

### Compiled TypeScript views (preview)

TypeScript apps compile Native markup beside the core by default in `native dev`, `native build`, `native test`, and direct Zig builds. Binding evaluation and portable component policies compile to native code through scriptc; the host supplies platform resources, drawing, input delivery, and replay. Unsupported markup produces a build diagnostic instead of switching backends.

In `native dev`, a markup edit replaces the running views in place. The Debug build stages a development view evaluator (`-Dview-reload`), and the dev loop recompiles each edit with the same view compiler and hands it to the app. The model, focus and the retained state of widgets whose identity is unchanged all persist. A broken edit prints its location and keeps the last good view. Adding or removing a window view or a virtual window, or changing `src/core.ts`, still needs a rebuild. Release builds never include the reload code. The dev loop also answers [`native automate provenance` and `edit`](/docs/automation#write-back) from the compiled views. An explicit `-Dtypescript-view=false` selects the native markup backend for reference comparisons.

The compiled view includes selection and keyboard policies for radios, tabs, trees, lists, menus, toggles, accordions, sliders, splits, scrolling, and text entry. The component pages describe their interaction contracts. Selection and disclosure state can be model-controlled; unchanged source values retain local control state, while changed source values take precedence. A changed slider source applies after an active drag ends.

Keyboard control activation also compiles beside the model. Enter/Space presses buttons and focusable composed controls, toggles boolean controls and accordions, and selects rows or segments. Shift remains eligible; Control, Alt, and Super leave activation to other routing. Closed select and combobox arrows open the picker, while mounted-menu arrows move focus. Tree selection and expansion honor the landed row, and radio-group navigation selects the focused radio. Native retains disabled-control checks, focus routing, state application, and OS input delivery.

Semantic control-intent resolution also compiles beside the model. Pointer handler dispatch uses the granted press, toggle, and select actions, including selectable tree rows and radio change handlers. Public semantic-control queries preserve action grants and select-versus-press precedence. Native keeps advertised accessibility actions, visibility checks, retained mutation, and value/scroll geometry; assistive activation continues through the same keyboard route.

Text fields use the SDK text reducer for UTF-8 editing, selection, word navigation, deletion, and composition. CRLF is one caret stop; edits that exceed capacity are refused as a whole. A changed source matching the edited text is an echo, while a different replacement replaces it. Explicit source selections remain authoritative. Native retains pointer hit testing, painted-line textarea navigation, IME delivery, clipboard transport, and history storage. The [Writing Desk example](https://github.com/vercel-labs/native/tree/main/examples/text-policy) demonstrates controlled text and textarea submission policies.

Escape and single-line Up/Down also compile beside the app model. Plain Escape cancels active composition before clearing a search field or combobox; other editors keep their text. Shift or navigation modifiers leave Escape to the app. Single-line Up/Down moves to the beginning/end, with Shift extending from the anchor. A closed combobox opens its picker first, without moving the caret. Native retains menu focus routing and textarea navigation through painted lines and soft wraps.

Copy and Cut compile the selected source-byte range, clamping and ordering endpoints before snapping them to UTF-8 boundaries. Missing or collapsed selections copy nothing; the default edit menu disables Cut and Copy accordingly and enables Select All for nonempty source. `textClipboardRange` from `@native-sdk/core/text` exposes the same range helper for app logic. Native owns clipboard reads and writes; Cut removes text only after a successful clipboard write. Code gutters and diff markers stay outside copied bytes.

Clipboard and history shortcut recognition also compiles beside the model: primary+C/X/V copies, cuts, or pastes; primary+Z undoes and primary+Shift+Z redoes. Primary is Command on macOS and Ctrl on other hosts. Shift/Alt clipboard chords and Alt history chords are ignored by these default commands. Native supplies host modifier facts and retains focus, clipboard transport, composition eligibility, and history storage.

Single-line input removes CR/LF from typing, paste, and IME previews. IME cursor offsets account for removed bytes. Paste applies the view text capacity after sanitizing and stops at a UTF-8 boundary, reporting truncation. Undo and redo compile selection, replacement, and selection restoration while replaying retained bytes exactly. They preserve selection direction and affinity, refuse stale history, and commit only after reaching the full target state. Native owns clipboard transport, history storage, and serial lookup.

Editable code accepts `<code source="{draft}" language="typescript" editable="true" wrap="false" line-numbers="true" on-input="edit_draft"/>`, where `draft` is UTF-8 bytes and `edit_draft` carries a `TextInputEvent`. Language names are literals; `line-numbers` accepts a boolean binding. Plain Tab inserts the file's inferred indentation: tabs when tab-indented lines dominate, otherwise two through eight spaces. Tied tab votes follow the caret's logical line; ambiguous sources use two spaces. Shift+Tab follows normal focus traversal. Line numbers stay outside source and clipboard bytes, and long lines scroll horizontally. Native retains syntax painting, text geometry, storage, IME and scroll delivery. Compiled code supports editable and read-only presentation, wrapping, width, height, min-width, grow, label and keys. The [Code Workbench example](https://github.com/vercel-labs/native/tree/main/examples/code-workbench) demonstrates independent drafts, source replacement and Undo/Redo.

Code diff annotations also compile beside the model. `added-lines="2-4, 7"` and `removed-lines="1"` accept literal one-based lists and inclusive ranges, or one UTF-8 byte binding. Lines are unique and must be from 1 through 128; added and removed sets must be disjoint. Invalid specs fail view evaluation, so apps accepting user input can validate it with `parseCodeLineNumberSpec` from `@native-sdk/core/text` before applying it. Native paints the full-width washes and `+`/`-` markers without adding bytes to source, selection or clipboard text. Annotations work with line numbers on or off and preserve editor history. Code Workbench includes editable line lists with Apply diff and Clear diff.

Debug and Release use the same generated view. Rebuild after markup edits; compiled views do not hot-reload markup.

Compiled views support `bubble`, `table`, `table-row`, `table-cell`, `progress`, `skeleton`, `spinner`, `combobox`, `grid`, `card`, `alert`, `dialog`, `drawer`, `sheet`, `resizable`, `tooltip`, `icon`, `media-surface`, `terminal`, `markdown`, `chart`, `input-group`, `column`, `row`, `stack`, `panel`, `badge`, `text`, `input`, `search-field`, `textarea`, `code`, `button`, `checkbox`, `switch`, `toggle`, `split`, `radio`, `radio-group`, `toggle-button`, `toggle-group`, `accordion`, `tabs`, `segmented-control`, `tree`, `list`, `list-item`, `select`, `dropdown-menu`, `menu-item`, `separator`, `status-bar`, `spacer`, `scroll`, and `avatar`. Bindings include numbers, booleans, byte text, record paths, helper-returned lists, and enum comparisons with literal members. Composition includes `if`/`else`, keyed `for`, imports, templates with required arguments, and slots. Integer and string `key` bindings preserve native widget identities across sibling reorders; `global-key` preserves them across reparenting too. Tree rows use `panel`, `column`, or `row` and declare `role="treeitem"`, `on-press`, optional `expanded`/`on-toggle`, and optional integer `tree-level` from 0 to 65535; `column`, `row`, `panel`, and `scroll` can also declare `role="tree"` for their rows.

Events include void and scalar payload bindings through `on-press`, `on-toggle`, `on-submit`, and menu `on-dismiss`, the thirteen-arm `TextInputEvent` union through `on-input`, the six-field drag record through `on-drag`, and the two-axis `ScrollState` record through `on-scroll`. The native runtime supplies live text edits, drag geometry, and scroll state. Layout attributes are `gap`, `padding`, `grow`, `width`, `height`, `min-width`, `main`, and `cross`, with `size`, `variant`, `checked`, `selected`, `disabled`, `value`, `image`, literal or text-bound `icon`, `label`, `role="listitem"`, and `window-drag`. Text leaves accept `wrap`; text-entry widgets and select triggers accept `text` and `placeholder`. Splits require exactly two panes after structural expansion. `on-resize` names a bare one-number float Msg arm; echo its applied fraction through `value`. Pane `min-width` bounds drag and keyboard resizing. Optional `resize-duration`, literal `resize-easing`, and literal `resize-origin` retain native animation and reduced-motion behavior. Accordion headers use `text`, with child elements for the body. Checkbox labels accept `text` or leaf content. Dropdown menus accept literal `anchor="below"` or `"above"`, `anchor-alignment="start"`, `"end"`, or `"stretch"`, and a finite literal `anchor-offset`; alignment and offset require `anchor`. Color attributes accept the full semantic token vocabulary; radii accept `sm`, `md`, `lg`, `xl`, and `none`. Pressable elements accept `quiet-hover` to suppress the hover wash. Unsupported constructs fail the build with a source diagnostic. Use `-Dtypescript-view=false` when an app needs the native markup backend.

`stepper`, `timeline`, and `timeline-item` also work in the preview. Their composition runs in TypeScript compiled by scriptc: stepper derives completed/active/pending indicators, bold or muted labels, selected state, and accessible list positions; timeline items derive badge/connector layout, wrapped descriptions, meta text, and focusability from `on-press`. Native still measures and renders the primitive widgets. Pressable timeline items activate with Enter or Space; items without `on-press` remain display-only. The [pipeline example](https://github.com/vercel-labs/native/tree/main/examples/pipeline) demonstrates stage changes, payload events, keyed timeline reordering, and empty states.

Secondary windows use the same preview path: export `windows(model)` with literal descriptor labels and provide `src/windows/<label>.native`. Each open window evaluates its bindings against the shared committed model. Window roots can import components under `src/windows/`; input routes by the descriptor's unique `canvasLabel`. Native close policies and `onCloseCommand` retain their usual behavior. The feed reader demonstrates shared URL editing, service updates, and close/reopen across both windows.

### Core types

```ts title="src/core.ts"
// Model: readonly data fields only.
export interface Model {
  readonly count: number;
}

// Msg: one arm per thing that can happen, at least two arms.
export type Msg =
  | { readonly kind: "add" }
  | { readonly kind: "reset" };

// Pure: the same model every time.
export function initialModel(): Model {
  return { count: 0 };
}

// One case per arm; the switch must be exhaustive (no default needed once
// every arm is present — a missing arm is a teaching error at build time).
export function update(model: Model, msg: Msg): Model {
  switch (msg.kind) {
    case "add":
      return { ...model, count: model.count + 1 };
    case "reset":
      return { ...model, count: 0 };
  }
}
```

`update` is pure and synchronous: it never mutates `model` or `msg`, never performs IO, and returns the next model — plus optionally command data describing effects (below). Purity is what makes the core testable as a plain function, replayable deterministically, and identical in behavior under node and native.

A complete core in the idiom — readonly interfaces, a tagged Msg, spread updates, map/filter, bytes for text, derived exports:

```ts title="src/core.ts"
import { utf8Bytes } from "@native-sdk/core";

export type Bytes = Uint8Array;
export type Filter = "all" | "active" | "done";

export interface Task {
  readonly id: number;
  readonly title: Bytes;
  readonly done: boolean;
}

export interface Model {
  readonly tasks: readonly Task[];
  readonly nextId: number;
  readonly filter: Filter;
  readonly draft: Bytes;
}

export type Msg =
  | { readonly kind: "add" }
  | { readonly kind: "toggle"; readonly id: number }
  | { readonly kind: "set_filter"; readonly filter: Filter }
  | { readonly kind: "draft_edit"; readonly text: Bytes };

export function initialModel(): Model {
  return {
    tasks: [{ id: 1, title: utf8Bytes("Ship the core"), done: false }],
    nextId: 2,
    filter: "all",
    draft: new Uint8Array(0),
  };
}

export function visibleTasks(model: Model): readonly Task[] {
  if (model.filter === "active") return model.tasks.filter((t) => !t.done);
  if (model.filter === "done") return model.tasks.filter((t) => t.done);
  return model.tasks;
}

export function doneCount(model: Model): number {
  return model.tasks.filter((t) => t.done).length;
}

export function update(model: Model, msg: Msg): Model {
  switch (msg.kind) {
    case "add": {
      if (model.draft.length === 0) return model;
      const task: Task = { id: model.nextId, title: model.draft, done: false };
      return {
        ...model,
        tasks: [...model.tasks, task],
        nextId: model.nextId + 1,
        draft: new Uint8Array(0),
      };
    }
    case "toggle":
      return {
        ...model,
        tasks: model.tasks.map((t) => (t.id === msg.id ? { ...t, done: !t.done } : t)),
      };
    case "set_filter":
      return { ...model, filter: msg.filter };
    case "draft_edit":
      return { ...model, draft: msg.text };
  }
}
```

Markup binds your model's field names exactly as you wrote them: `nextId` binds as `{nextId}` (the core's model keeps the TS spellings), string-literal unions bind as their member name (`{filter}` renders `all`), and record arrays iterate with `<for each="tasks" as="t" key="id">`. Exported helpers taking exactly one `Model` parameter join the binding surface as derived values — `{doneCount}` reads `doneCount`, and slice-returning ones like `visibleTasks` drive `for each` — so derived data needs no model field. Update-only state nothing in markup binds (host-fired timer arms, bookkeeping fields) is declared once as `export const viewUnbound = ["tick"] as const;` so `native check`'s unbound-state lint can distinguish intentional omissions.

<span id="why-the-immutable-style-is-free" />

## Model memory

Values created by `update` use a per-dispatch arena, released after the returned model commits. The commit copies newly created nodes into the persistent model heap and shares unchanged nodes from the previous model.

The frame arena and model heap each have a fixed build-time capacity of 1 MiB by default. The frame arena bounds transient values for one dispatch; the model heap bounds committed state. Capacity overflow produces a runtime panic identifying the region.

<span id="the-subset-posture" />

## Supported TypeScript

The core checker admits a deterministic TypeScript subset. Supported features include readonly records, discriminated unions, exhaustive switches, loops, arithmetic and assignment operators, record destructuring, namespace imports, spreads, array transforms, `Math`, and template literals encoded as bytes. Generic functions, interfaces, and aliases compile to an implementation for each resolved type instantiation. Data classes compile to records and functions; inheritance is unavailable. Tagged subset values can use `throw`, `try`, `catch`, and `finally`; an uncaught throw panics.

Mutation is allowed on locally owned data, such as a scratch array or `.slice()` copy, until it escapes. Shared model and message data remain immutable. Readonly reader parameters borrow arrays without transferring ownership.

The core excludes npm imports, regular expressions, `JSON`, Promises, `eval`, inheritance, `async`/`await`, `Map`/`Set`, mutable module state, ambient time and randomness, and runtime type tests. Dynamic text uses bytes. Diagnostics identify the rule and a supported alternative. Put ordinary imperative TypeScript behind a [service request](/docs/typescript/services).

Checker rules have two classifications. `guarantee` protects core invariants such as determinism, fixed data shapes, immutable shared state, and byte text. `deferred` marks a currently unavailable capability that does not inherently violate those invariants. This classification describes current rules; it is not a release schedule.

- `guarantee` — core invariants: NS1001 (shared data is immutable), NS1002 (updates are synchronous), NS1005 (update is deterministic), NS1010 (module state lives in the Model), the byte-text rules (NS1004, NS1018, NS1024, NS1060), and every other rule not listed as deferred.
- `deferred` — currently unavailable: NS1011 (`Map`/`Set`), NS1019 (fixed arity: parameter defaults, rest, `arguments`, call spreads), NS1040 (regular expressions), NS1042 (generators), NS1044 (`BigInt`/`Symbol`).

```ts
// One generic helper; tsc resolves each call's type arguments.
export interface Task { readonly id: number; readonly done: boolean; }
export function pick<T>(xs: readonly T[], i: number): T {
  return xs[i];
}
export function firstTask(tasks: readonly Task[]): Task { return pick(tasks, 0); }
export function lastNum(ns: readonly number[]): number { return pick(ns, ns.length - 1); }
```

```zig
// The Zig-core equivalent: one monomorphic fn per distinct instantiation.
pub fn pick__Task(xs: []const Task, i: i64) Task {
    return xs[uz(i)];
}
pub fn pick__f64(xs: []const f64, i: i64) f64 {
    return xs[uz(i)];
}
```

One rule deserves calling out early: **text is bytes**. Dynamic, user-visible text lives in the Model as `Uint8Array` — `string` is for literals, string-literal-union tags, and `===` comparisons. Turn display literals and templates into UTF-8 with `utf8Bytes`; use `asciiBytes` only when ASCII is part of the value's contract, such as a command name, key, or protocol token:

```ts
import { asciiBytes, utf8Bytes } from "@native-sdk/core";

const label = utf8Bytes(`${done} of ${total} done`); // per-dispatch UTF-8
const seed = utf8Bytes("Café…");                    // UTF-8 rodata
const command = asciiBytes("app.refresh");          // guaranteed ASCII
```

The compiler folds both byte intrinsics at compile time; under node the same imports run as plain functions with the same result. `asciiBytes` fails with NS1064 when a literal/template contains non-ASCII and throws `RangeError` if called directly with such text under node. `utf8Bytes` encodes Unicode exactly like `TextEncoder`, including U+FFFD for lone surrogates. Observing a `string`'s code units (`.length`, `s[i]`) is a taught error because UTF-16 and UTF-8 would disagree, and `+` concatenation is taught away because runtime string building needs a JS string heap the binary does not carry.

Bytes still read like text: the everyday string methods work directly on `Uint8Array` values, with **byte-based semantics** — every length, offset, and index is a BYTE length/offset (never a character count: `é` measures 2), search is byte-wise, and case mapping is Unicode simple case mapping (code point to code point from the Unicode tables, locale-free, no special casing — `ß` stays `ß`; invalid UTF-8 passes through unchanged). The compiled core and node run the same methods from the same generated tables, so both produce identical bytes by construction.

<table>
  <thead>
    <tr>
      <th>
        Method
      </th>

      <th>
        Byte-based behavior
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `toUpperCase()`

        /

        `toLowerCase()`
      </td>

      <td>
        Unicode simple case mapping over UTF-8 (locale-free); fresh bytes
      </td>
    </tr>

    <tr>
      <td>
        `repeat(n)`
      </td>

      <td>
        The bytes repeated

        `n`

        times;

        `repeat(0)`

        is empty, a negative literal is a build error (JS throws RangeError there)
      </td>
    </tr>

    <tr>
      <td>
        `startsWith(b)`

        /

        `endsWith(b)`

        /

        `includes(b)`
      </td>

      <td>
        Byte-wise prefix/suffix/substring tests with a bytes needle;

        `includes(65)`

        with a number keeps TypedArray element search — one byte value
      </td>
    </tr>

    <tr>
      <td>
        `indexOf(b)`

        /

        `lastIndexOf(b)`
      </td>

      <td>
        First/last BYTE offset of the byte substring, -1 when absent (a number argument searches one byte value)
      </td>
    </tr>

    <tr>
      <td>
        `padStart(n, fill?)`

        /

        `padEnd(n, fill?)`
      </td>

      <td>
        Pad to

        `n`

        BYTES (not characters) with fill bytes (default

        `" "`

        ), last repetition truncated by bytes
      </td>
    </tr>

    <tr>
      <td>
        `trim()`

        /

        `trimStart()`

        /

        `trimEnd()`
      </td>

      <td>
        Strip the JS whitespace set decoded over UTF-8; a view, no copy
      </td>
    </tr>

    <tr>
      <td>
        `split(sep)`
      </td>

      <td>
        Split on a bytes separator into

        `Uint8Array[]`

        (String.split shapes; the parts array is yours to mutate); an empty separator literal is a taught stop
      </td>
    </tr>

    <tr>
      <td>
        `at(i)`
      </td>

      <td>
        The byte value at a byte index (negatives count from the end), or

        `undefined`

        out of range
      </td>
    </tr>
  </tbody>
</table>

What stays out teaches its reason and the byte-based alternative by name: `charCodeAt`/`charAt`/`codePointAt` read UTF-16 code units bytes do not have (read `b[i]`/`.at(i)`), the locale family (`localeCompare`, `toLocaleUpperCase`) depends on ambient locale state, the regex-taking methods (`match`, `search`) need a regex engine the binary does not carry, and `normalize`/`replace`/`replaceAll` are named deferrals with their rewrites.

## Effects are Cmd data

`update` never performs an effect — it can return one, as inert data, alongside the next model. Declare the pair-return type and build commands inline in the return path (never stored in the model, a message, or a local — that is what keeps effect descriptions outside app state):

```ts title="src/core.ts"
import { Cmd } from "@native-sdk/core";

export interface Model {
  readonly count: number;
  readonly lastTick: number;
}

export type Msg =
  | { readonly kind: "add" }
  | { readonly kind: "request_time" }
  | { readonly kind: "tick"; readonly at: number };

export const viewUnbound = ["tick", "lastTick"] as const;

export function initialModel(): Model {
  return { count: 0, lastTick: -1 };
}

export function update(model: Model, msg: Msg): Model | [Model, Cmd<Msg>] {
  switch (msg.kind) {
    case "add":
      return { ...model, count: model.count + 1 };
    case "request_time":
      return [model, Cmd.now("tick")]; // dispatches { kind: "tick", at: <ms> }
    case "tick":
      return { ...model, lastTick: msg.at }; // bare model means no command
  }
}
```

The runtime interprets the command after the model commits and dispatches any result back as an ordinary `Msg` — routing is data (string-literal arm names, never callbacks), so the result decoder derives from your Msg types at build time. `initialModel` may return the same pair to run one boot effect before the first view build (loading a store is the canonical use). The vocabulary:

<table>
  <thead>
    <tr>
      <th>
        Command
      </th>

      <th>
        What it does
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `Cmd.none`
      </td>

      <td>
        No effects; returning a bare

        `Model`

        is sugar for it
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.now("tick")`
      </td>

      <td>
        Request a timestamp; dispatches the named arm with the time (ms) as its one number payload
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.delay(key, ms, "fired")`
      </td>

      <td>
        A keyed one-shot timer; re-issuing a live key re-arms it from now — the debounce discipline
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.readFile(path, { key?, ok, err })`
      </td>

      <td>
        Read a whole file;

        `ok`

        carries the content bytes,

        `err`

        a reason (

        `not_found`

        ,

        `io_failed`

        ,

        `truncated`

        , ...)
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.writeFile(path, bytes, { key?, ok, err })`
      </td>

      <td>
        Write a whole file (parents created, replaced whole);

        `ok`

        carries no payload — a successful write has nothing to report
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.appendFile`

        /

        `Cmd.statFile`

        /

        `Cmd.deleteFile`
      </td>

      <td>
        Append one bounded payload, inspect

        `{ exists, size, mtimeMs }`

        , or delete one file with explicit

        `not_found`

        handling
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.readFileStream`

        /

        `Cmd.writeFileStream`

        \+

        `writeFileChunk`

        /

        `writeFileClose`
      </td>

      <td>
        Read 256-KiB chunks without a total-size cliff, or build an atomic export one acknowledged chunk at a time — see

        <a href="/docs/files">Files & Streaming</a>
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.fetch(spec, { key?, ok, err })`
      </td>

      <td>
        A buffered HTTP(S) exchange;

        `ok`

        carries

        `{ status, body }`

        (a 404 is still

        `ok`

        — a delivered response),

        `err`

        the transport reason
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.fetch(spec, { key?, line, ok, err })`
      </td>

      <td>
        A line-streamed HTTP(S) exchange for SSE/NDJSON; each

        `line`

        carries bytes as it arrives, then

        `ok`

        carries the terminal HTTP status or

        `err`

        the transport reason
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.clipboardWrite(bytes)`

        /

        `Cmd.clipboardRead({ key?, ok, err })`
      </td>

      <td>
        System clipboard: write is fire-and-forget, read routes the text bytes back
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.showNotification({ id?, title, subtitle?, body?, actionLabel?, actionCommand? })`
      </td>

      <td>
        Show or replace a desktop notification, fire-and-forget; paired action fields dispatch through the ordinary app-command path while the process is running
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.openExternalUrl(url)`

        /

        `Cmd.revealPath(path)`
      </td>

      <td>
        Open an allowed HTTP(S) URL in the system browser or reveal a path in Finder/Files/Explorer; both are fire-and-forget and fail closed
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.credentials.set(...)`

        /

        `Cmd.credentials.get(...)`

        /

        `Cmd.credentials.delete(...)`
      </td>

      <td>
        App-scoped access to the OS credential store; get returns secret bytes, missing items route

        `miss`

        , and the manifest must declare the

        `credentials`

        capability and permission
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.formatLocalTime(timestampMs, style, route)`
      </td>

      <td>
        Format an epoch timestamp as localized

        `date`

        ,

        `time`

        , or

        `datetime`

        text in the host's current time zone
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.spawn(argv, { key?, stdin?, line?, exit, err })`
      </td>

      <td>
        Run a subprocess, streaming stdout line by line;

        `collect: true`

        buffers whole stdout into the

        `exit`

        arm instead
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.audioPlay(key, source, { event })`

        \+

        `audioPause`

        /

        `audioResume`

        /

        `audioStop`

        /

        `audioSeek`

        /

        `audioSetVolume`
      </td>

      <td>
        The audio player: one event stream (

        `loaded`

        ,

        `position`

        ,

        `completed`

        ,

        `failed`

        ,

        `spectrum`

        , ...) until

        `audioStop`

        closes it
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.showWindow(label)`

        /

        `Cmd.hideWindow(label)`

        /

        `Cmd.quitApp()`
      </td>

      <td>
        The menu-bar lifecycle verbs: show or retain-but-hide the labeled window, and gracefully terminate the app
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.setDockPresence(visible)`
      </td>

      <td>
        Switch macOS between regular Dock/app-switcher presence and accessory/headless behavior; unsupported hosts ignore it
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.launchAtLoginStatus(route)`

        /

        `Cmd.setLaunchAtLogin(enabled, route)`
      </td>

      <td>
        Query or change the installed app bundle's

        `SMAppService`

        registration; the ok bytes name

        `enabled`

        ,

        `disabled`

        ,

        `requires_approval`

        , or

        `not_found`
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.imageLoad(id, source, { event })`

        \+

        `imageCancel(id)`

        /

        `imageUnregister(id)`
      </td>

      <td>
        Load an image at runtime under the model-owned numeric ImageId your markup binds; one

        `event`

        result —

        `loaded`

        with the decoded width/height, or a failure class.

        `imageCancel`

        ends a live load loudly (state

        `cancelled`

        ) and frees the id;

        `imageUnregister`

        releases a loaded image's registry slot (no result — synchronous, like registration) — see

        <a href="/docs/dynamic-images">Dynamic Images</a>
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.channelOpen(key, { event })`

        /

        `channelClose(key)`
      </td>

      <td>
        Open an external-source channel under an app-chosen numeric key: the native side holds the posting handle and feeds bytes from its own threads, and every post arrives through the one

        `event`

        arm with the back-pressure counters aboard.

        `channelClose`

        flushes staged posts, dispatches exactly one

        `closed`

        event with the final drop totals, and frees the key
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.audioCaptureStart(key, spec, { event })`

        /

        `audioCaptureStop(key)`
      </td>

      <td>
        Capture

        `microphone`

        or

        `system`

        audio as bounded, timestamped, interleaved signed-16 LE PCM chunks. The stream reports

        `started`

        ,

        `data`

        ,

        `failed`

        ,

        `stopped`

        , and

        `rejected`

        , with observable drop counters
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.persist()`
      </td>

      <td>
        Snapshot the just-committed Model through the engine-owned, capability-gated atomic store; restore arrives through the manifest's configured boot Msg route — see

        <a href="/docs/persistence">Model Persistence</a>
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.store.set/get/delete/scan/setMany`
      </td>

      <td>
        Persist independent byte records in the engine-owned, capability-gated record store; every result returns through the declared Msg route — see

        <a href="/docs/record-store">Record Store</a>
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.db.query(sql, params, route)`

        /

        `Cmd.db.exec(statements, route)`
      </td>

      <td>
        Run read-only relational queries as bounded row pages or commit a statement list atomically through the engine-owned SQLite database — see

        <a href="/docs/sqlite">Relational SQLite</a>
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.host(name, ...args)`

        /

        `Cmd.request(name, payload, { key?, ok, err })`
      </td>

      <td>
        App-defined host commands by literal name: fire-and-forget, or routed with exactly one result Msg back
      </td>
    </tr>

    <tr>
      <td>
        `Cmd.cancel(key)`

        /

        `Cmd.batch([a, b])`
      </td>

      <td>
        Drop an in-flight keyed effect; issue several commands from one dispatch, in order
      </td>
    </tr>
  </tbody>
</table>

Result arms must match the effect payload: bytes for raw host results and errors, a generated record for typed service results, numbers for timer fires and stream totals, no payload for write acknowledgments, and `{ status, body }` for buffered fetches.

Duplicate keys and cancellation depend on the effect. Buffered engine effects and streamed file reads replace a live predecessor and cancel silently. Service requests reject live duplicate keys, including buffered requests; cancelling a buffered service request dispatches no result. Spawn, streaming-fetch, streaming-service, and streamed write-sink operations reject duplicate keys and route cancellation through `err`. A streaming fetch with a cut or dropped line ends with `truncated`. Check each operation's reference before reusing a key.

`Cmd.openExternalUrl(url)` enforces the [external-link policy](/docs/security#external-links), and `Cmd.revealPath(path)` opens the desktop file manager. `Cmd.credentials.set(key, secret, route)`, `.get(key, route)`, and `.delete(key, route)` use a string key within the namespace derived from the app manifest id. Set/delete use payload-free success arms; get returns secret bytes. A missing get routes `miss`; deleting an absent key succeeds. Declare both the `credentials` capability and permission.

`Cmd.formatLocalTime(timestampMs, "date" | "time" | "datetime", route)` returns localized UTF-8 text using the host locale and time zone. Recording captures the result, so replay does not read those ambient settings.

Durable in-memory state uses `Cmd.persist()`: declare the `persist` capability, configure the boot routes and schema version, then return the command beside the committed model. The engine owns canonical serialization, trailing-edge coalescing, atomic app-data placement, backup recovery, migration, and journal/replay. See [Model Persistence](/docs/persistence) for the complete setup. Raw file commands remain for user-visible files, exports, and blobs; [Files & Streaming](/docs/files) covers their bounds, atomic sink protocol, replay, and `filesystem` permission gate.

Independent byte records use `Cmd.store`: declare the `store` capability, then route set/get/delete/scan/setMany results back to Msg arms. The engine owns the app-data path, SQLite schema, atomic batches, pagination, and replay boundary. See [Record Store](/docs/record-store).

External sources — sockets, file watchers, native worker threads — reach `update` through a channel. `Cmd.channelOpen(key, { event })` opens a long-lived stream under an app-chosen numeric key, and every event dispatches the one `event` arm as a five-field record; `state` must be a named string-literal-union alias carrying exactly the three members — a narrower union would silently drop states the host emits, so the build refuses it. Posting is not a TS verb: compiled cores are single-threaded by design, so the posting handle lives on the native side (`Effects.channelHandle(key)`), where embedders and platform-services extensions post bytes from their own threads. Back-pressure is observable — posts the native handle refused count into `droppedPending`/`droppedTotal` on the next delivered event, never silence — and a duplicate open on a live key dispatches `rejected`. `Cmd.channelClose(key)` ends the stream: staged posts flush, exactly one `closed` event carries the final totals, and the key frees.

```ts title="src/core.ts"
import { Cmd } from "@native-sdk/core";

export type ChannelState = "data" | "closed" | "rejected";

export interface Model {
  readonly samples: number;
  readonly dropped: number;
  readonly live: boolean;
}

export type Msg =
  | { readonly kind: "start" }
  | { readonly kind: "stop" }
  | {
      readonly kind: "feed";
      readonly key: number;
      readonly state: ChannelState;
      readonly bytes: Uint8Array;
      readonly droppedPending: number;
      readonly droppedTotal: number;
    };

export const viewUnbound = ["feed"] as const;

export function initialModel(): Model {
  return { samples: 0, dropped: 0, live: false };
}

export function update(model: Model, msg: Msg): Model | [Model, Cmd<Msg>] {
  switch (msg.kind) {
    case "start":
      return [{ ...model, live: true }, Cmd.channelOpen(1, { event: "feed" })];
    case "stop":
      return [model, Cmd.channelClose(1)];
    case "feed":
      switch (msg.state) {
        case "data":
          return { ...model, samples: model.samples + 1, dropped: msg.droppedTotal };
        case "closed":
          return { ...model, live: false, dropped: msg.droppedTotal };
        case "rejected":
          return { ...model, live: false };
      }
  }
}
```

Audio input uses the same bounded, wake-driven stream transport without requiring native posting code. `Cmd.audioCaptureStart(key, { source, sampleRate?, channels? }, { event })` captures the microphone or the desktop output mix. The supported canonical rates are 16, 24, and 48 kHz; channels are mono or stereo; the default is 48 kHz mono. Each `data` event carries at most 20 ms of interleaved signed 16-bit little-endian PCM in `pcm`, plus `timestampMs`, `frames`, the delivered format, and drop counters. Microphone and system capture can run concurrently, but only one stream per source is live; starting that source again stops the prior key. `Cmd.audioCaptureStop(key)` quiesces the native callback, drains accepted chunks, then emits one `stopped` terminal. A key remains occupied until that terminal is delivered, so wait for `stopped` before reusing it. Add `"microphone"` and/or `"system_audio"` to the `app.json` permissions array so packaged macOS apps receive the required usage descriptions and consent prompts.

```ts title="src/core.ts"
import { Cmd, type AudioCaptureState, type AudioCaptureSource } from "@native-sdk/core";

export type Msg =
  | { readonly kind: "record" }
  | { readonly kind: "stop" }
  | { readonly kind: "audio_chunk"; readonly key: number; readonly state: AudioCaptureState; readonly source: AudioCaptureSource; readonly sampleRate: number; readonly channels: number; readonly timestampMs: number; readonly frames: number; readonly pcm: Uint8Array; readonly droppedPending: number; readonly droppedTotal: number };

// In update:
// case "record": return [model, Cmd.audioCaptureStart(1, { source: "microphone", sampleRate: 48000, channels: 1 }, { event: "audio_chunk" })];
// case "stop": return [model, Cmd.audioCaptureStop(1)];
```

## Model-derived menu-bar status items

A `src/core.ts` app can own its complete native menu-bar item without custom Zig wiring. Export `statusItem(model): StatusItemState`; the generated launcher installs its icon, tooltip, click/open commands, presentation, and rows from the boot model, then re-derives all of them after committed updates. It patches shell, presentation, and menu independently and never recreates the item just because model state changed.

```ts title="src/core.ts"
import { asciiBytes, utf8Bytes } from "@native-sdk/core";
import { type StatusItemState } from "@native-sdk/core/events";

export function statusItem(model: Model): StatusItemState {
  return {
    iconPath: asciiBytes("assets/menu-bar.svg"),
    tooltip: utf8Bytes("Sync status"),
    activationCommand: asciiBytes("app.sync"),
    alternateActivationCommand: asciiBytes(""),
    openCommand: asciiBytes("app.sync"),
    presentation: { title: model.syncing ? utf8Bytes("SYNC…") : utf8Bytes("READY"), width: 62, tone: model.failed ? "critical" : "normal", iconOpacity: model.stale ? 0.5 : 1, monospaced: true, fontSize: 13, fontWeight: "semibold" },
    items: [
      { id: 1, label: utf8Bytes("Open"), command: asciiBytes("app.open"), separator: false, enabled: true, detail: asciiBytes(""), role: "command", key: asciiBytes(""), modifiers: { primary: false, command: false, control: false, option: false, shift: false } },
      { id: 2, label: utf8Bytes("Sync now…"), command: asciiBytes("app.sync"), separator: false, enabled: !model.syncing, detail: asciiBytes(""), role: "command", key: asciiBytes("r"), modifiers: { primary: true, command: false, control: false, option: false, shift: false } },
    ],
  };
}
```

Import the canonical records and unions from `@native-sdk/core/events`. Presentation includes byte `title`, numeric `width`, tone, `iconOpacity`, `monospaced`, `fontSize`, and `fontWeight`; `statusItems` composes several independently styled persistent menu-bar items. Rows include id/label/command/separator/enabled plus secondary `detail`, semantic `role`, key equivalent, and all five modifier booleans. Actionable ids are unique and non-zero, and there are at most 32 rows. `commandMsg(name): Msg | null` maps row selection, status-button activation, Option-activation, and menu-open refresh into the ordinary update loop. See [System Tray](/docs/tray).

Export `statusItems(model): readonly StatusItemDescriptor[]` when the app needs several independent items. Each descriptor adds stable non-zero `id` identity and a live `visible` flag to the same shell/presentation/menu record. Adding/removing descriptors creates/removes only those ids; icon, title, tooltip, visibility, activation/open commands, and menu changes patch in place. Export either the singular or collection helper, not both. macOS supports up to eight simultaneous items; every item keeps its own 32-row menu.

## Model-declared secondary windows

Export `windows(model): readonly WindowDescriptor[]` to derive the live secondary-window set from model state. Construct entries with `windowDescriptor` from `@native-sdk/core`, import `WindowDescriptor` from `@native-sdk/core/events`, and put each window's markup at `src/windows/<label>.native`. Spell the constructor label as a literal `label: asciiBytes("<label>")`; `native check` and every build reject dynamic labels or a label without that matching root. Window roots can import shared components nested under `src/windows/`; the generated launcher embeds and hot-reloads the complete import closure. Adding/removing descriptors creates/closes only those windows; all open windows rebuild from the same committed model.

The main `src/app.native` view can import component files anywhere under `src/`, with `src/components/` as the convention. The generated launcher embeds every other `.native` source and watches the imported closure, so the same imports work in development, release builds, and mobile builds. Secondary-window roots remain narrower: they import within `src/windows/`.

`closePolicy` accepts `"quit"` (the default) or `"hide"`. A `"quit"` user close routes `onCloseCommand` through `commandMsg`, where the app maps it to the Msg that clears its open flag. A `"hide"` close retains the same native window and view and dispatches no close command; `Cmd.showWindow(label)` reveals it. Model-declared secondary windows are desktop-only. See `examples/system-monitor`.

`restorePolicy` accepts `"clamp_to_visible_screen"` (the default) or `"center_on_primary"`. Model-declared windows do not restore persisted frames. On macOS, `"center_on_primary"` centers a fresh descriptor with no authored `x`/`y`; Windows and Linux currently keep their native default placement.

`titlebar` accepts `"standard"`, `"hidden_inset"`, `"hidden_inset_tall"`, or `"chromeless"`. Transparent Windows windows require `"chromeless"`; because that removes the system buttons, fully skinned windows must draw working close/minimize controls.

## Subscriptions are Sub data

Recurring effects are declared, not issued: export `subscriptions(model): Sub<Msg>` and return descriptors derived from the current model. After every commit the host reconciles the returned set against its active timers by key — a new key (or a changed interval) arms a timer, a missing key cancels it — so starting, stopping, and re-tuning timers is just returning different data:

```ts title="src/core.ts"
import { Sub } from "@native-sdk/core";

export interface Model {
  readonly running: boolean;
  readonly fast: boolean;
  readonly ticks: number;
}

export type Msg =
  | { readonly kind: "toggle" }
  | { readonly kind: "set_fast" }
  | { readonly kind: "tick"; readonly at: number };

export const viewUnbound = ["tick"] as const;

export function initialModel(): Model {
  return { running: false, fast: false, ticks: 0 };
}

export function update(model: Model, msg: Msg): Model {
  switch (msg.kind) {
    case "toggle":
      return { ...model, running: !model.running };
    case "set_fast":
      return { ...model, fast: true };
    case "tick":
      return { ...model, ticks: model.ticks + 1 };
  }
}

export function subscriptions(model: Model): Sub<Msg> {
  if (!model.running) return Sub.none;
  return Sub.batch([
    Sub.timer("tick", model.fast ? 250 : 1000, "tick"),
    Sub.timer("autosave", 30000, "tick"),
  ]);
}
```

Keep the Sub-vs-stream line straight: a Sub is declarative — derived from the model, started and stopped by reconciliation, never opened or closed by the app. The multi-result streams (`Cmd.fetch`'s response lines, `Cmd.spawn`'s stdout lines, `Cmd.audioPlay`'s events, `Cmd.channelOpen`'s posts, and audio capture chunks) are Cmd-initiated — imperative opens with a keyed lifecycle the app drives. If the effect should exist exactly while some model state holds, it wants a Sub; if the app decides when it starts and ends, it is a stream.

## Text input from markup

A markup text control (`<text-field text="{draft}" on-input="draft_edit" />`) needs a bytes field the control renders and a Msg arm carrying the text-input event. The event union mirrors the runtime's event vocabulary structurally — import it (`import { type TextInputEvent } from "@native-sdk/core/text"`, also re-exported by `@native-sdk/core/events`) or declare the same shape in your core; the pairing rules are in [Native UI: Messages](/docs/native-ui#messages). Your `update` reduces the events over the draft bytes; a minimal reducer (append / backspace / clear) covers simple fields. Full caret/selection/IME fidelity is one import: the SDK ships the byte-splice text engine as `@native-sdk/core/text` (below).

## Splitting a core into modules

A core that outgrows one file splits into modules under `src/` except `src/services/`: relative imports spelled with their real filenames (`./parsers.ts` — the same file runs under node, whose loader resolves real files), `src/` as the hard boundary (`../` and npm packages are diagnostics), and no runtime cycles (`import type` back-edges are fine and idiomatic — a helper module typically type-imports `Model` from the entry). The core may not import service files, even type-only; shared subset-legal shapes live in an ordinary core-class module which a service may import. Export lists and value re-exports are ordinary module surface: `export { helper, doneCount as remaining }` binds names over existing declarations, and `export { parsePs } from "./parsers.ts"` forwards another module's export by name — what stays out is `export default`, `export =`, and `export * from` (the core's flat namespace resolves by name, so every export names what it binds). `core.ts` stays the entry module and the app's public face: `update`, `initialModel`, `subscriptions`, the wiring channels, `themeState` / `themePack` / `statusItem` / `statusItems` / `windows`, and the exported binding helpers live there (declared and exported under their own names — a rename or re-export cannot bind an entry point), and imported modules hold the machinery they call. The SDK also ships library modules in the same subset — `@native-sdk/core/text` is the byte-splice text engine (caret, selection, IME composition, ASCII case-insensitive compare), and `@native-sdk/core/events` is the canonical event and shell vocabulary (`TextInputEvent` re-exported, `ScrollState`, `FrameEvent`, `KeyEvent`, `PinchPhase`/`PinchEvent`, `ColorScheme`, `ThemeState`, the chrome records, `AudioState`/`AudioEvent`, status-item records, and `WindowDescriptor`) so no core re-types it — compiled into your core when imported and absent when not.

```ts
// src/core.ts — the entry module: Model, Msg, update, and the exports
// markup binds. Imports feed them.
import { parseSample, type Sample } from "./parsers.ts";
import { containsIgnoreCase } from "@native-sdk/core/text";

// src/parsers.ts — a module of the same core, plain subset TypeScript:
import type { Model } from "./core.ts"; // type-only back-edges are legal
export interface Sample { readonly value: number; }
export function parseSample(bytes: Uint8Array): Sample | null { /* ... */ }
```

```zig
// src/main.zig — a Zig core splits the ordinary way:
const parsers = @import("parsers.zig");

// src/parsers.zig
pub const Sample = struct { value: i64 };
pub fn parseSample(bytes: []const u8) ?Sample { ... }
```

## TypeScript services

The core is the app's deterministic logic — `Model`, `Msg`, `update`; services do the app's imperative work. A service operation is a directly exported, non-default named synchronous function under `src/services/`, taking zero or one explicitly typed request and declaring a contract-encodable result. Crossing shapes live in an exported, subset-legal module outside `src/services/` so the core and service import one declaration. The operation name is `<module-basename>.<export>`; `native check` projects its complete type table into `services.contract.json`, checks both classes, and generates the typed core client:

```ts title="src/core.ts"
import { feedsParse } from "@native-sdk/services";

case "parse":
  return [
    model,
    feedsParse({ source: model.source, caseSensitive: false }, {
      key: "parse",
      ok: "parsed",       // one ParseResult field
      err: "parse_failed", // one Uint8Array field
    }),
  ];
```

The core never receives a synchronous handle. Its update returns a command, the typed result crosses back as the named Msg arm, and the runtime journals that result like every other effect. Replay parks the request and feeds the recorded result without starting the carrier. The service itself is ordinary static-tier TypeScript — Node built-ins, `fetch`, regexes, JSON, `Map`/`Set`, `Date`, classes — running with the app's privileges on a supervised, lazily started carrier: a sibling child process with a sanitized environment by default, or an explicitly selected in-process worker-thread pool. Writing operations, kind-tagged error throws, streaming and cancellation, exact vendored npm, and the boundary rules NS1065–NS1067 have their own chapter: [TypeScript Services](/docs/typescript/services).

## The dev loop

`native dev --core` runs logic without rebuilding the native app: the core runs under Node with a virtual host — dispatch Msgs as JSON lines (`{"kind":"add"}`, `{"$bytes":"…"}` for bytes payloads), advance a virtual clock (`{"advance":1000}`) to fire timers deterministically, and watch the committed model and effect transcript. Service requests run in an isolated Node worker through the same generated contract: vendored hashes are verified, request/results use the same codecs and error arms, cooperative cancellation/deadlines interrupt CPU-bound work, and stream chunks use the same channel-event shape. Pair `--script msgs.ndjson` with `--watch` to replay a scenario on every edit. The devhost also consumes `NATIVE_SDK_SESSION_RECORD`/`NATIVE_SDK_SESSION_REPLAY` (the environment set by `native automate record|replay`): it writes the native journal format, and replay starts no service worker. Service-only recordings cross between it and the packaged runtime; packaged recordings containing other effect families use `native automate replay`, and devhost rejects those records explicitly. `native dev` runs compiled services for real beside the native app. [Quick Start](/docs/quick-start#run-the-core-under-nodejs) shows a full transcript.

`native dev` hot-reloads `.native` edits into the running window. A `src/core.ts` edit rebuilds the core through the external core compiler and restarts the app. Use `native dev --core` to check logic without rebuilding, then run `native dev` to test the compiled app.

`native check` runs the subset checker (real tsc semantics plus the app-core rules) over the core class, emits and validates the service contract, runs the pinned compiler's coverage verdict over each independent service root, then validates markup and the app manifest. Diagnostics identify the rule and a supported rewrite.

## Build targets

Builds compile everything in the app — the core archive, any service executables or in-process archives, and the runner — for one stated target. The default is the build host; `-Dtarget` selects a cross desktop target following the pinned compiler's build matrix: Linux and Windows GNU targets build from any macOS, Linux, or Windows host, and macOS targets build on a macOS host (Apple linking needs the host toolchain's SDK). A Windows MSVC target builds natively on a matching Windows host; cross-Windows builds use the GNU ABI because Zig supplies that target's CRT and system libraries. An explicitly spelled Linux `-gnu` target also states its glibc version — `x86_64-linux-gnu.2.36` or later, or `x86_64-linux-musl` — because the compiled runtime needs glibc 2.36+ (a bare `-gnu` spelling lands on Zig's older default floor and is refused with the same teaching). The executable name and packaging follow the target OS.

Mobile targets compile the same core as a static archive merged into the mobile embed library, which `native dev|package --target ios|android` link into the toolkit hosts exactly as they do for Zig cores. The mobile matrix is aarch64 only: `aarch64-ios` and `aarch64-ios-simulator` build on a macOS host against the selected Apple SDK with an iOS 15.0 floor, and `aarch64-linux-android` builds on any desktop host against an installed NDK (`ANDROID_NDK_ROOT`, or the newest `ndk/<version>` under the SDK) with an API 26 floor. Services on mobile run only on the in-process pool — mobile apps cannot spawn a sibling process — so `service_carrier = "auto"` resolves to the pool there and an explicit `"child"` is refused with a teaching; desktop builds of the same app keep the child carrier under `auto`. The vendored npm lane is unchanged. Model persistence (`persist`), boot images, and URL media caching are not wired on mobile yet.

## Editor support

The scaffold includes `package.json` and `tsconfig.json` for editor typechecking with tsc. `@native-sdk/core` and its subpaths resolve through `node_modules`. The CLI materializes and refreshes the editor package from the SDK it ships; `native doctor` reports version skew. Apps with services also get an ignored `node_modules/@native-sdk/services` package when `native check` or `native dev --core` regenerates the client. Builds resolve the shipped SDK and checked sources directly, rather than relying on the editor packages.

## Outgrowing the subset

The compiled core is a native static archive, not generated source: `native check` checks and leaves nothing behind, and there is no emitted Zig to read or adopt. If logic needs ambient APIs or ordinary static-tier TypeScript, keep deterministic state transitions in the core and move that work into `src/services/`. Port the core to Zig only when the logic tier itself needs capabilities outside both TypeScript classes; the [App Model](/docs/app-model) page covers that wiring.

## Where the subset ends

The core owns app state and decisions — the app's deterministic logic. Services own imperative application work in ordinary TypeScript: parsing, filesystem transforms, environment inspection, subprocesses, and other ambient operations whose results cross back as messages. The toolkit-extension tier — custom widgets, rasterizer work, new engine-owned effects, and platform integration — remains Zig by design: that layer is the machinery itself, and [Building Components](/docs/building-components) is its guide. Services are not a backdoor storage engine or general FFI surface.

## Reference

The complete core guide ships as `native skills get ts-core`; typed service contracts, vendored npm, streaming, authority, and transport limits ship as `native skills get ts-services`. Both are written for AI agents and precise enough for humans.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: Where Packages Go
url: "https://native-sdk.dev/docs/typescript/packages"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The app core uses a checked, deterministic TypeScript subset and compiles to native code. It does not run npm packages or ship a JavaScript engine. Choose where library code runs according to what it needs: a compiled service, an HTTP effect, an embedded web frontend, a Node subprocess, or a compatible local core module.

Use the following options:

<table>
  <thead>
    <tr>
      <th>
        You want
      </th>

      <th>
        The pattern
      </th>

      <th>
        Where the code runs
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        Filesystem, JSON/regex parsing, transforms, or imperative work in ordinary TypeScript
      </td>

      <td>
        A compiled module under

        `src/services/`
      </td>

      <td>
        A native service-host process, reached through

        `@native-sdk/services`

        commands
      </td>
    </tr>

    <tr>
      <td>
        An HTTP API — including AI/LLM endpoints
      </td>

      <td>
        `Cmd.fetch`

        with routed results
      </td>

      <td>
        The effect engine, in the binary
      </td>
    </tr>

    <tr>
      <td>
        An npm-heavy UI (an editor, a charting stack, an existing React app)
      </td>

      <td>
        Embed a web frontend
      </td>

      <td>
        A WebView surface, full npm
      </td>
    </tr>

    <tr>
      <td>
        A Node library for one job
      </td>

      <td>
        `Cmd.spawn`

        a node sidecar
      </td>

      <td>
        A subprocess, streaming lines back
      </td>
    </tr>

    <tr>
      <td>
        A pure utility (parsing, formatting, math)
      </td>

      <td>
        Usually nothing — or vendor it under

        `src/`
      </td>

      <td>
        Inside the core, compiled to native
      </td>
    </tr>
  </tbody>
</table>

## Compiled TypeScript services

Put ordinary static-tier TypeScript under `src/services/` when the work needs Node built-ins, regexes, JSON, `Map`/`Set`, `Date`, classes, or ambient process authority. Each directly exported, non-default named synchronous function becomes an operation named `<module-basename>.<export>`. Its request and result may be shared, contract-encodable records; the core calls the generated constructor from `@native-sdk/services`, so success and failure still arrive as Msgs and record/replay remains offline.

Services compile to native code. `native vendor . package@X.Y.Z` resolves an exact package graph with lifecycle scripts disabled and writes checked-in sources and hashes to the app manifest. Builds verify those sources without installing packages. `native check` requires complete static coverage from the pinned compiler; unsupported packages fail with its diagnostics. Desktop services use a child process by default, with an in-process option on supported targets. See [TypeScript Services](/docs/typescript/services) for platform support, typed calls, streaming, cancellation, and authority. The [feed reader example](https://github.com/vercel-labs/native/tree/main/examples/service-feed-reader) fetches a feed and parses it through a typed service operation.

## Calling APIs, AI endpoints included

`Cmd.fetch` sends HTTP requests without an API-client package. Use a buffered request for one `{ status, body }` result, or line streaming for SSE and NDJSON. Results return as messages and are recorded for offline replay. This core sends a buffered request to an OpenAI-compatible chat endpoint:

```ts title="src/core.ts"
import { Cmd, asciiBytes } from "@native-sdk/core";

export interface Model {
  readonly answer: Uint8Array;
  readonly waiting: boolean;
}

export type Msg =
  | { readonly kind: "ask" }
  | { readonly kind: "answered"; readonly status: number; readonly body: Uint8Array }
  | { readonly kind: "ask_failed"; readonly reason: Uint8Array };

export const viewUnbound = ["answered", "ask_failed"] as const;

export function initialModel(): Model {
  return { answer: new Uint8Array(0), waiting: false };
}

export function update(model: Model, msg: Msg): Model | [Model, Cmd<Msg>] {
  switch (msg.kind) {
    case "ask":
      if (model.waiting) return model; // one request in flight, by model state
      return [
        { ...model, waiting: true },
        Cmd.fetch(
          {
            url: asciiBytes("http://127.0.0.1:11434/v1/chat/completions"),
            method: "POST",
            headers: { "content-type": "application/json" },
            body: asciiBytes('{"model":"local-model","messages":[{"role":"user","content":"Say hi in five words"}]}'),
            timeoutMs: 120000,
          },
          { key: "chat", ok: "answered", err: "ask_failed" },
        ),
      ];
    case "answered":
      // The status is the real HTTP status - a 404 is a delivered
      // response. This sample displays the raw body; the chatbot
      // example includes response parsing.
      return { ...model, waiting: false, answer: msg.body };
    case "ask_failed":
      // The transport reason ("timed_out", "connect_failed", ...) -
      // failure is never silence.
      return { ...model, waiting: false, answer: msg.reason };
  }
}
```

```zig
// The same exchange on the Zig effects channel.
.ask => fx.fetch(.{
    .key = chat_key,
    .method = .POST,
    .url = "http://127.0.0.1:11434/v1/chat/completions",
    .headers = &.{.{ .name = "content-type", .value = "application/json" }},
    .body = "{\"model\":\"local-model\",\"messages\":[...]}",
    .on_response = Effects.responseMsg(.answered),
}),
.answered => |response| model.recordAnswer(response), // copy response.body
```

For token-by-token UI, request the endpoint's streaming mode and add a `line` route:

```ts
Cmd.fetch(
  {
    url: endpoint,
    method: "POST",
    headers: { accept: "text/event-stream", authorization: bearerToken },
    body: requestBody,
    timeoutMs: 120000,
    maxLineBytes: 65536,
  },
  { key: "chat", line: "chat_event", ok: "chat_done", err: "chat_failed" },
)
```

`chat_event` carries one `Uint8Array` field for each complete SSE/NDJSON line; parse its `data:` payload and append the delta to the assistant message in the Model. `chat_done` carries one number field with the terminal HTTP status. Cancellation and transport failures reach `chat_failed` as reason bytes, including `cancelled`, so a partially displayed answer never ends silently.

The [chatbot example](https://github.com/vercel-labs/native/tree/main/examples/chatbot) streams responses from Vercel AI Gateway, reads credentials through the environment effect, and appends response deltas to model state. It includes request construction, response parsing, cancellation, and replay tests.

## Full npm ecosystem UIs

Use a [web frontend](/docs/frontend) for an existing React app, editor component, or other UI library that needs a JavaScript engine. The WebView runs inside the native shell, and the [bridge](/docs/bridge) carries typed messages between it and the app. The scaffold configures development-server loading and production asset bundling:

```bash
native init my_app --frontend next   # or vite | react | svelte | vue
```

npm lives in the web surface, where a JS engine actually exists; the native tier keeps the window, menus, tray, dialogs, and everything else the frontend cannot reach. [Web Content](/docs/frontend) covers the full setup, including production asset bundling.

## Node as a worker

`Cmd.spawn` can run a Node script for a discrete job, such as rendering a template or transforming a file. Send input through stdin and receive stdout lines as messages. The argv must be a compile-time array literal. Lines are bounded at 4 KiB; `collect: true` buffers output up to 512 KiB instead. **The deployed host must have Node installed**, and the executable path must match that host.

```ts
case "render":
  return [
    { ...model, rendering: true },
    Cmd.spawn([asciiBytes("/usr/local/bin/node"), asciiBytes("scripts/render.mjs")], {
      key: "render",
      stdin: model.template,
      line: "render_line",
      exit: "render_done",
      err: "render_failed",
    }),
  ];
```

Cancellation (`Cmd.cancel("render")`), the exit code on the `exit` arm, and machine-readable failure reasons all follow the standard effect contract — see [the streaming ops](/docs/typescript#effects-are-cmd-data). `examples/system-monitor` is the reference for the collect shape: spawn a tool, parse its whole output in the core.

## Pure utilities

For parsing, formatting, or math helpers, choose one of the reuse options below. Core helpers must fit the checked subset; text values use `Uint8Array`.

```ts title="src/core.ts"
import { parseCsvRow } from "./csv.ts";                    // vendored under src/
import { containsIgnoreCase } from "@native-sdk/core/text"; // the SDK library channel
```

- **Vendor subset-clean code under `src/` outside `src/services/`.** It compiles into the core like your own modules ([splitting a core into modules](/docs/typescript#splitting-a-core-into-modules)); the subset checker tells you immediately — by rule ID, with the rewrite — whether it fits.
- **Vendor ordinary static-tier code or exact npm packages under `src/services/`.** Classes, regexes, JSON, `Map`/`Set`, `Date`, Node built-ins, and imperative transforms stay in TypeScript and compile into the service host; run `native vendor . package@X.Y.Z` for a package, then reach its typed exported operations through `@native-sdk/services`.
- **`@native-sdk/core/*` is the curated library channel**: SDK modules written in the same subset, compiled into your core when imported and absent when not. Today that is `@native-sdk/core/text` — the byte-splice text engine (caret, selection, IME, case-insensitive search) — and `@native-sdk/core/events` — the canonical event record types markup and the wiring channels match. See `examples/chatbot/src/api.ts` for local helpers that process JSON over bytes.

Core builds resolve local modules under `src/` and the shipped SDK modules. Service packages must be vendored under `src/services/` and pinned by name, version, and tree hash in the app manifest. Builds do not install packages or fetch sources.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)

---
title: TypeScript Services
url: "https://native-sdk.dev/docs/typescript/services"
docs_index: /llms.txt
lastUpdated: 2026-10-16
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Modules under `src/services/` are ordinary TypeScript compiled to native code on the compiler's full static tier: `fs`, `path`, `process`, `os`, `child_process`, `fetch`, regexes, `JSON`, `Map`/`Set`, `Date`, and classes, when the pinned compiler supports them. The same pinned compiler builds the deterministic core ([TypeScript Cores](/docs/typescript)) and the services; no JavaScript engine ships in either.

The core calls a service by returning a command from `update`. The typed result returns as an ordinary `Msg`:

```ts title="src/core.ts"
import { feedsParse } from "@native-sdk/services";

case "parse":
  return [model, feedsParse({ source: model.source, caseSensitive: false }, {
    key: "parse",
    ok: "parsed",        // the one Msg arm carrying ParseResult
    err: "parse_failed", // a one-Uint8Array-field arm
  })];
```

Services run on a supervised carrier — on desktop, a separate child process by default or an explicitly selected worker-thread pool compiled into the app binary; on iOS and Android, the in-process pool only (see [Runtime behavior](#runtime-behavior)).

## The two roles

The split is by role: the core is the app's deterministic logic — `Model`, `Msg`, `update` — and services do the app's imperative work. Record→replay, headless testing, and [automation](/docs/automation) depend on `update` being a pure function of its inputs. A service reads the real filesystem, clock, and network, so the checker refuses a core import of a service file (NS1065) and the core-to-service edge is always a command. Service results are journaled like every other effect result.

<table>
  <thead>
    <tr>
      <th>
        Role
      </th>

      <th>
        Owns
      </th>

      <th>
        Language rules
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        Core (

        `src/core.ts`

        \+ imports outside

        `src/services/`

        )
      </td>

      <td>
        App state and decisions:

        `Model`

        ,

        `Msg`

        ,

        `update`

        , pure helpers
      </td>

      <td>
        The deterministic subset (NS1001–NS1064)
      </td>
    </tr>

    <tr>
      <td>
        Service (

        `src/services/**/*.ts`

        )
      </td>

      <td>
        Imperative work: parsing, filesystem transforms, environment inspection, subprocesses
      </td>

      <td>
        Ordinary static-tier TypeScript; only the boundary rules NS1065–NS1067 apply
      </td>
    </tr>
  </tbody>
</table>

Services are not a storage engine or a general FFI surface. Durable data uses the engine-owned [persistence](/docs/persistence) and [record store](/docs/record-store) effects; custom widgets, render passes, and new engine capabilities are Zig ([Building Components](/docs/building-components)).

## Service authority

A service runs with the app's privileges. The child carrier uses the app data directory as its working directory and the environment allowlist below. The in-process carrier shares the app's working directory and environment. Neither carrier is an OS security sandbox. Services may use supported APIs for:

- **Filesystem** — Node built-ins over the real disk.
- **Environment** — `process` and the allowlisted variables below.
- **Network** — `fetch` and sockets, directly.
- **Ambient time and randomness** — `Date.now()`, `Math.random()`, and friends. Their results reach the core only as journaled message payloads.

The child process receives an explicit environment allowlist; everything else, including every `NATIVE_SDK_*` internal, is stripped.

<table>
  <thead>
    <tr>
      <th>
        Group
      </th>

      <th>
        Variables
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        Path
      </td>

      <td>
        `PATH`
      </td>
    </tr>

    <tr>
      <td>
        Home / user / temp
      </td>

      <td>
        `HOME`

        ,

        `USER`

        ,

        `TMPDIR`

        ,

        `TMP`

        ,

        `TEMP`
      </td>
    </tr>

    <tr>
      <td>
        Locale / time zone
      </td>

      <td>
        `LANG`

        ,

        `LC_ALL`

        ,

        `LC_CTYPE`

        ,

        `TZ`
      </td>
    </tr>

    <tr>
      <td>
        Certificates
      </td>

      <td>
        `SSL_CERT_FILE`

        ,

        `SSL_CERT_DIR`
      </td>
    </tr>

    <tr>
      <td>
        Proxies
      </td>

      <td>
        `HTTP_PROXY`

        ,

        `HTTPS_PROXY`

        ,

        `NO_PROXY`
      </td>
    </tr>

    <tr>
      <td>
        Windows additions
      </td>

      <td>
        `USERPROFILE`

        ,

        `USERNAME`

        ,

        `SystemRoot`

        ,

        `COMSPEC`

        ,

        `PATHEXT`

        ; all names match case-insensitively
      </td>
    </tr>
  </tbody>
</table>

Standard output carries the framed transport between app and service, so service diagnostics go to standard error.

## Writing a service

A service module is any `.ts` file under `src/services/`. Every directly exported, non-default named function is an operation:

- It is synchronous and has a body.
- It takes zero or one explicitly annotated request parameter.
- It declares a contract-encodable result type.
- Its name is `<module-basename>.<export>` — `export function parse` in `src/services/feeds.ts` is `feeds.parse`.

Boundary shapes live in a shared, subset-legal module outside `src/services/`, imported by the core and the service:

```ts title="src/shared.ts"
export type ParseRequest = {
  readonly source: Uint8Array;
  readonly caseSensitive: boolean;
};

export type ParseResult = {
  readonly bytes: Uint8Array;
  readonly matches: boolean;
};
```

```ts title="src/services/feeds.ts"
import * as fs from "node:fs";
import type { ParseRequest, ParseResult } from "../shared.ts";

export function parse(request: ParseRequest): ParseResult {
  if (!fs.existsSync(".")) {
    throw { kind: "data_directory_missing", message: "the app data directory is unavailable" };
  }
  const source = new TextDecoder().decode(request.source);
  const matches = request.caseSensitive ? /feed/.test(source) : /feed/i.test(source);
  return { bytes: new TextEncoder().encode(JSON.stringify({ matches })), matches };
}
```

`native check` projects the complete type table into a contract sidecar (`services.contract.json`), checks both classes, and generates the typed client the core imports. An operation shaped any other way — `async`, a default export, an unannotated request, a non-encodable result — is a teaching error (NS1067) naming the rewrite.

### Boundary types

<table>
  <thead>
    <tr>
      <th>
        Crosses
      </th>

      <th>
        Notes
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        Booleans, numbers
      </td>

      <td>
        Integer-class fields are proven and carried as integers
      </td>
    </tr>

    <tr>
      <td>
        `Uint8Array`
      </td>

      <td>
        The bytes form the core and services already share
      </td>
    </tr>

    <tr>
      <td>
        Optionals, readonly slices
      </td>

      <td>
        `T | null`

        and

        `readonly T[]`

        of encodable elements
      </td>
    </tr>

    <tr>
      <td>
        Named records, enums, kind-tagged unions
      </td>

      <td>
        Declared in the shared module; both sides import the one declaration
      </td>
    </tr>

    <tr>
      <td>
        Functions, behavior-bearing classes, Promises
      </td>

      <td>
        Do not cross — the boundary is encoded data, not object references
      </td>
    </tr>
  </tbody>
</table>

Services may use classes, `Map`, and other APIs supported by the pinned compiler. Request and result types must still satisfy the boundary contract.

### Errors

An explicit throw crossing the operation boundary must be exactly an inline `{ kind: "...", message: "..." }` shape with a string-valued message, and it must escape the operation rather than be caught locally:

```ts
throw { kind: "parse", message: "bad feed" };
```

The encoded kind and message arrive on the core's error arm as UTF-8 JSON bytes. Do not throw `new Error(...)` from the exported surface. The build mechanically lowers the escaping tagged value into the form the pinned compiler carries across the boundary; your checked-in source — and its behavior under Node — does not change.

## Calling a service

`native check` derives the virtual module `@native-sdk/services` from the contract: one constructor per operation, named `<module><Export>` (`feeds.parse` → `feedsParse`), taking the typed request plus a route.

```ts title="src/core.ts"
import { feedsParse } from "@native-sdk/services";
import type { ParseRequest, ParseResult } from "./shared.ts";

export type Msg =
  | { readonly kind: "parse"; readonly request: ParseRequest }
  | { readonly kind: "parsed"; readonly result: ParseResult }
  | { readonly kind: "parse_failed"; readonly error: Uint8Array };

case "parse":
  return [model, feedsParse(msg.request, {
    key: "feed-parse",
    ok: "parsed",
    err: "parse_failed",
  })];
```

The route is typechecked: the constructor's type proves that `ok` names the one Msg arm carrying exactly the declared result record and that `err` names a one-bytes-field arm. A stale field or wrong route is a `native check` type error at the call site. The generated source lives only in build scratch space and the ignored editor package under `node_modules/@native-sdk/services`, never in authored `src/`.

Raw `Cmd.request("feeds.parse", bytes, { key?, ok, err })` remains the low-level byte seam beneath the client — same transport, same routing, request and result as raw bytes you encode yourself.

### Keys

Keys share the engine effect-key space. A second live request on the same key — buffered or streaming — is rejected (`err` receives `rejected`) rather than replacing the first, so two calls can never splice into one result. Cancel the first if you mean to supersede it.

### Timeouts

Every request carries a deadline: 30 seconds by default, or the operation's declared `@deadlineMs` (a JSDoc tag, 1 to 86400000 ms). Expiry routes JSON with `kind: "timeout"` to `err`.

### Cancellation

`Cmd.cancel(key)` on a buffered request drops it — no message is dispatched — and cooperatively interrupts the service child. Cancelling a stream routes `cancelled` to `err` (see [Streaming](#streaming)).

## Streaming

To return incremental results, declare a final typed `emit` capability. Each chunk arrives through a channel-event Msg arm; the function's return stays the one typed terminal result.

```ts title="src/services/feeds.ts"
import type { ServiceCancellation } from "@native-sdk/core";
import type { ParseChunk, ParseRequest, ParseResult } from "../shared.ts";

/**
 * @deadlineMs 5000
 * @streamBuffer 8
 */
export function parseLarge(
  request: ParseRequest,
  emit: (chunk: ParseChunk) => void,
  cancellation: ServiceCancellation,
): ParseResult {
  for (let index = 0; index < request.source.length; index += 4096) {
    cancellation.throwIfCancelled();
    emit({ bytes: request.source.slice(index, index + 4096), index });
  }
  return parse(request);
}
```

The generated route gains two fields beside `key`, `ok`, and `err`: `channelKey` (an app-chosen numeric channel key) and `event` (the channel-event Msg arm each chunk dispatches). The terminal result closes the channel after all accepted chunks. `@streamBuffer` caps in-flight chunks at 1–64 (default 8).

### Cooperative cancellation

An optional final `ServiceCancellation` parameter opts an operation into cooperative cancellation — legal only as the last parameter. Poll `cancelled()` or call `throwIfCancelled()` at bounded intervals.

- `Cmd.cancel(key)` on a stream flips the token, closes the channel, routes `cancelled` to `err`, and drops every later chunk.
- A deadline expiry flips the same token and routes `kind: "timeout"` to `err`.
- The child gets a short grace period to unwind and stays alive when it cooperates. An operation that ignores its token is hard-killed, and the next request starts a clean host.

## npm packages

Service modules may import local service files, shared core-class declarations, and exact vendored npm packages — never a bare install:

```bash
native vendor . escape-string-regexp@5.0.0
```

The command resolves the exact version once (lifecycle scripts disabled), copies the flattened package graph and license files into `src/services/vendor/`, and writes the exact name/version/tree-hash facts into the app manifest. Check both in. Builds are hermetic: no npm, no network — every vendored byte is re-hashed, and the compiler receives only the explicit declared package allowlist. Importing a package that was never vendored is NS1066:

> Run `native vendor . package@X.Y.Z`, check in `src/services/vendor/` and the generated manifest `service_packages` facts, then import that exact package name; or vendor a local source module and import it relatively.

A vendored package must reach 100% static coverage with the pinned compiler. Otherwise, `native check` fails and includes the compiler's coverage diagnostics. This is a build requirement, not a general claim about npm compatibility.

Run `native check` to verify the exact package sources you vendored. There is no automatic or dynamic fallback. If a package is refused, use a compatible implementation or move the work to a [web frontend or Node worker](/docs/typescript/packages). Local service modules must also compile on the pinned static tier.

## Runtime behavior

Two carriers run the same operations behind the same routes, keys, deadlines, cancellation, streaming, and replay semantics. The build selects one:

<table>
  <thead>
    <tr>
      <th>
        Carrier
      </th>

      <th>
        Where services run
      </th>

      <th>
        Selection
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        `child`
      </td>

      <td>
        A second native executable —

        `<app>_services`

        — beside the app binary, packaged with it
      </td>

      <td>
        Unset/

        `auto`

        default on desktop; unavailable on mobile
      </td>
    </tr>

    <tr>
      <td>
        `in_process`
      </td>

      <td>
        Compiled into the app binary; a small thread pool, one isolated module instance per thread
      </td>

      <td>
        Explicit opt-in on native Linux, cross-Linux x86\_64/aarch64, native Windows x86\_64, cross-Windows x86\_64 GNU, or macOS built on macOS; unset/

        `auto`

        default on iOS and Android
      </td>
    </tr>
  </tbody>
</table>

`.service_carrier = "in_process" | "child"` in app.zon (or `-Dservice-carrier`) states the choice. Unset/`"auto"` selects the child carrier on desktop and the in-process pool on iOS/Android, where a child process is unavailable. `.service_pool_size` (or `-Dservice-pool-size`, 1-16) sets the in-process pool width; the default is min(4, cores).

Shared guarantees:

- **Lazy start.** Nothing starts before the first real request — no child process, no pool thread.
- **Verified pairing.** The child's startup handshake checks the protocol version and a fingerprint of the generated operation/type/package registry; the in-process carrier checks the same fingerprint against the linked archive. A mismatch rejects before any operation dispatches.
- **Supervision.** A second live request with the same key is rejected. After cancellation, an abandoned dispatch retains the key until it stops; replacement work waits behind it. The in-process pool runs different keys in parallel across its instances; the child runs everything on one worker. A cancellation or deadline publishes the cooperative token and grants a short grace: an operation that returns inside it keeps its instance (or process) warm. Past the grace, the child is killed and respawns on the next request; the in-process carrier abandons the instance's thread, routes the failure, and adds a fresh instance to the pool. An abandoned dispatch keeps its key reserved until it physically stops, so a same-key replacement cannot overlap its side effects (and can itself expire while waiting). A detected trap poisons only the instance it fired in (`kind: "service_trap"`); other instances keep answering. Every failure produces a routed result: a dead transport `kind: "service_host"`, an expired deadline `kind: "timeout"`.
- **Replay.** Terminal results and stream events are journaled like every other effect. Replaying a recorded session parks each request and feeds the recorded result; neither carrier starts anything.
- **Scope.** Child executables are desktop-only and follow the pinned compiler's broad matrix: same-platform builds, Linux and Windows GNU targets cross-compiled from a macOS/Linux/Windows host, and macOS targets built on macOS. A Windows MSVC target builds natively on a matching Windows host; cross-Windows uses GNU because Zig supplies that target's CRT and system libraries. In-process archives use the compiler's narrower object-localization matrix: native Linux, cross-Linux x86\_64/aarch64 (`aarch64-linux-android` included, API 26 floor, NDK required), native Windows x86\_64, cross-Windows x86\_64 GNU, or the Mach-O targets — macOS, `aarch64-ios`, and `aarch64-ios-simulator` (iOS 15.0 floor) — built on macOS. Mobile targets are archive-only: no sibling process exists there, so `service_carrier = "auto"` resolves to the in-process pool and an explicit `"child"` is refused with a teaching. A pairing outside the relevant matrix fails with a teaching, as does any explicitly spelled Linux `-gnu` target without a glibc version — even when it matches the build host, Zig's target uses its default floor. The service runtime needs glibc 2.36+ or musl, so explicit Linux targets are spelled `x86_64-linux-gnu.2.36` (or later) or `x86_64-linux-musl`. Operations are synchronous.

In-process specifics:

- Service code shares the app process: its ambient authority is the app's own (no environment allowlist, the app's working directory), and a hardware fault in service code — a stack overflow above all — is process-wide. Use the child carrier to keep service hardware faults outside the app process.
- Each pool worker owns a separate instance of the service modules. Mutable module globals are worker-local, so different-key requests may observe different copies; keep shared durable state outside service-module globals.
- An abandoned instance's memory is reclaimed only at process exit; each trap or ignored token costs one leaked instance.
- `process.exit()` in service code exits the app.

## Development

`native dev --core` runs service operations in an isolated Node worker through the same generated contract: the same vendored-package hash verification, request/result codecs, error arms, cooperative cancellation and deadlines, and channel-event chunk shape. Pair `--script scenario.ndjson` with `--watch` for repeatable iteration.

The devhost honors session record/replay the same way the packaged runtime does — replay starts no service worker — and service-only recordings cross between the devhost and the packaged app. `native dev` runs the app with its build-selected carrier: the in-process pool linked into the binary, or the compiled service executable beside it.

## Boundary diagnostics

Three checker rules enforce the boundary. Each diagnostic identifies the invalid boundary and the required change.

<table>
  <thead>
    <tr>
      <th>
        Rule
      </th>

      <th>
        Reason
      </th>
    </tr>
  </thead>

  <tbody>
    <tr>
      <td>
        <strong>NS1065</strong>

        — the core does not import services
      </td>

      <td>
        A direct import would run ambient, non-deterministic service authority inside update and erase the command/result boundary that journaling and replay depend on. The core-to-service edge is always an effect.
      </td>
    </tr>

    <tr>
      <td>
        <strong>NS1066</strong>

        — service package imports are exact vendored facts
      </td>

      <td>
        Service builds have no package-manager or network input: the compiler sees only manifest-declared, hash-verified checked-in sources through an explicit static-package allowlist.
      </td>
    </tr>

    <tr>
      <td>
        <strong>NS1067</strong>

        — service calls match the generated typed contract
      </td>

      <td>
        The host codecs, runner registry, and typed client are projections of

        `services.contract.json`

        ; every crossing data shape, stream declaration, deadline, and operation name must be stated there once.
      </td>
    </tr>
  </tbody>
</table>

## Reference

The [feed reader example](https://github.com/vercel-labs/native/tree/main/examples/service-feed-reader) fetches a feed, passes its bytes to `feeds.parse`, and renders typed `FeedResult` records. Invalid input returns a tagged error. Its tests record the loop and replay it without a service executable or network. Use `native skills get ts-services` for the CLI authoring guide.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)