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