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