# Native UI markup

> Elements, attributes, flex layout, bindings, expressions, and editor/LSP support for .native views compiled into the binary.

- Repository: vercel-labs/native
- GitHub: https://github.com/vercel-labs/native
- Human docs: https://grok-wiki.com/public/docs/vercel-labs-native-8ccc3580636a
- Complete Markdown: https://grok-wiki.com/public/docs/vercel-labs-native-8ccc3580636a/llms-full.txt

## Source Files

- `skill-data/native-ui/SKILL.md`
- `editors/native-markup/README.md`
- `editors/native-markup/extension.js`
- `src/runtime/canvas_widget_runtime.zig`
- `docs/src/app/native-ui/layout.tsx`

---

---
title: "Native UI markup"
description: "Elements, attributes, flex layout, bindings, expressions, and editor/LSP support for .native views compiled into the binary."
---

`.native` files declare the entire canvas widget tree for a native-rendered app: elements, flex layout, value bindings, message dispatch, and structure tags. At build time the source is either interpreted at runtime (`canvas.MarkupView`) or compiled at comptime (`canvas.CompiledMarkupView` / `CompiledMarkupImports`) into the same `canvas.Ui(Msg)` nodes a hand-written Zig builder would emit—identical structural widget ids and the same typed handler table. Markup never mutates state; it only reads the `Model` and dispatches `Msg` values. Logic lives in Zig (`update` / `update_fx`).

```text
src/<view>.native  ──parse──►  MarkupDocument
       │                         │
       │              ┌──────────┴──────────┐
       │              ▼                     ▼
       │     MarkupView (dev/hot reload)   CompiledMarkupView (release)
       │              │                     │
       └──────────────┴─────────► Ui(Msg).Tree ──► canvas_widget_runtime reconcile
```

## App wiring

A native UI app is markup plus TEA-shaped Zig:

| Piece | Role |
| --- | --- |
| `src/<view>.native` | View: elements, layout, bindings, `on-*` dispatch |
| `Model` | Plain struct; only source-of-truth fields |
| `Msg` | Tagged union of events |
| `update` / `update_fx` | Sole place state changes |

Wire the view through `native_sdk.UiApp` / `UiAppWithFeatures`. Release builds prefer comptime compilation and gate the interpreter with `.runtime_markup`:

```zig
const dev = @import("builtin").mode == .Debug;
const App = native_sdk.UiAppWithFeatures(Model, Msg, .{ .runtime_markup = dev });
const CompiledView = canvas.CompiledMarkupView(Model, Msg, @embedFile("habits.native"));

// Options:
.view = CompiledView.build,
.markup = if (dev) .{
    .source = @embedFile("habits.native"),
    .watch_path = "src/habits.native",
    .io = init.io,
} else null,
```

<ParamField body="source" type="[]const u8" required>
Embedded root markup bytes. Baseline for the first build and for watch comparison.
</ParamField>

<ParamField body="sources" type="[]const SourceFile" optional>
Import closure for multi-file documents (same set as `CompiledMarkupImports`). Paths are relative to the root file's directory.
</ParamField>

<ParamField body="watch_path" type="?[]const u8" optional>
Dev-only disk path to poll. When the root or any resolved import changes, the next rebuild swaps the view while preserving model state and widget ids. Omit in release.
</ParamField>

<ParamField body="io" type="?std.Io" optional>
Required when `watch_path` is set so the runtime can read files on the watch timer.
</ParamField>

With both `.view` (compiled) and `.markup` (watched) in Debug, the compiled tree renders until the watched file first changes, then the interpreter hot-reloads. Parse failures keep the last good view and set `app_state.markup_diagnostic` (path, line, column, message). Hybrid Zig roots that embed compiled fragments register each with `Options.fragment_watch` via `CompiledView.fragment("src/header.native")`—only Debug carries path/baseline data.

Reference app: `examples/habits` (`habits.native` + `CompiledHabitsView` + optional `watch_path`).

## Elements

Markup tags lower to canvas widget kinds. Structure tags (`for`, `if`, `else`, `template`, `use`, `import`, `slot`) are not widgets.

| Markup | Widget role | Notes |
| --- | --- | --- |
| `row`, `column` | Flex containers | Main axis horizontal / vertical |
| `stack`, `panel`, `card` | Overlay containers | Children layer; `gap` is a validation error |
| `scroll` | Scroll view | Put multi-child content in an inner `column`/`row` |
| `list`, `grid` | List / grid | Vertical stack / cell grid; `columns` on `grid` only |
| `tabs`, `toggle-group`, `button-group`, `radio-group`, `breadcrumb`, `pagination` | Horizontal row containers | Tab strips, chips, radios |
| `table` → `table-row` → `table-cell` | Table | Strict nesting; cells are text leaves |
| `dropdown-menu` | Menu surface | Children are `menu-item`s; `anchor` floats against parent |
| `accordion` | Accordion | Header via `text`; children while `selected` |
| `alert`, `bubble` | Surfaces | `alert` title via `text`; bubble message is content, not `text=` |
| `dialog`, `drawer`, `sheet` | Modal surfaces | In-place; show with `<if>` |
| `resizable` | Resizable panel | Engine drag handle; `width` is initial |
| `split` | Two-pane splitter | Exactly two element children; engine divider; `value` + `on-resize` |
| `tree` | Disclosure tree | Rows with `role="treeitem"` join one roving focus set |
| `text`, `badge`, `tooltip` | Text leaves | `{}` interpolation; `wrap` / `overflow` / `size` on `text` |
| `text` → `span` | Inline runs | Weight, mono, italic, scale, underline, foreground |
| `button`, `toggle-button`, `list-item`, `menu-item`, `toggle`, `switch`, `select`, `avatar` | Text-bearing controls | Label is content; optional `icon=` on several |
| `checkbox`, `radio`, `slider`, `progress` | Value controls | Labels via `text=` / `label=`; content text is an error |
| `text-field`, `input`, `search-field`, `combobox`, `textarea` | Text entry | `on-input` / `on-submit`; search-field has built-in clear |
| `status-bar` | Status bar | Content only |
| `separator`, `spacer` | Divider / flex space | Separator axis follows parent flex direction |
| `skeleton`, `spinner` | Loading leaves | Size skeleton with `width`/`height` |
| `icon` | Vector icon | Built-in name, `app:<name>`, or one `{binding}` |
| `markdown` | Markdown subtree | `source="{binding}"` only |
| `stepper` → `step`, `timeline` → `timeline-item` | Pipeline composites | Stage track / ledger list |
| `chart` → `series` | Data chart | `values="{binding}"` to `[]const f32` |
| `context-menu` | Parent metadata | Direct child of pressable parent; not flow content |
| `input-group` → `textarea` + `input-group-actions` | Grouped composer | One border; group owns focus ring |

**Not expressible in markup** (Zig `canvas.Ui` only): free-form `image` widgets (pixels are runtime `ImageId`s), `data_grid`, `popover` / `menu_surface`, `segmented_control` (use `tabs`/`toggle-group`), windowed virtual lists (`ui.virtualWindow` / `ui.virtualList`), `on_double_press`, and dynamic chart series composition / `.band` series. Avatars do bind images: `image="{binding}"` to a model `u64` ImageId (`0` keeps initials).

## Flex layout and attributes

### Layout attributes

| Attribute | Applies to | Behavior |
| --- | --- | --- |
| `gap` | Flow containers; `split` divider thickness | Invalid on stacking kinds (`stack`/`panel`/`card`/`alert`/`bubble`/`dialog`/`drawer`/`sheet`/`resizable`) |
| `padding` | Most containers | Uniform inset |
| `grow` | Flex children | Share free space on the main axis |
| `width`, `height` | Sized elements | Definite size; content neither shrinks nor silently overflows |
| `min-width` | Flex children; split panes | Floor without a definite max; bounds split drag |
| `main` | Flex containers | `start` \| `center` \| `end` \| `space_between` |
| `cross` | Flex containers | `stretch` \| `start` \| `center` \| `end` |
| `wrap` | `text` only | `"true"` word-wraps; unset/`false` is single-line |
| `overflow` | `text` only | `ellipsis` (default) or `clip` |
| `text-alignment` | Text leaves, titles | `start` \| `center` \| `end` |
| `columns` | `grid` only | Fixed column count |
| `virtualized`, `virtual-item-extent` | `scroll`/`list`/`grid`/`table` | Layout-culls mounted nodes (not full windowed virtual lists) |
| `anchor`, `anchor-alignment`, `anchor-offset` | `dropdown-menu` | Float against parent; late z-pass; window-clipped |
| `overscroll` | `scroll` only | `none` (default pin) or `rubber_band` |
| `window-drag` | Any element | macOS hidden-titlebar window move surface |

Numbers are plain (`gap="12"`). Booleans are `true`/`false` or a binding. When children's minimum sizes exceed a container, debug builds log `zero_canvas_layout` with container, axis, and overflow pixels—flex overflow is never silent.

### Appearance and state

Token attributes take **token names only** (no raw colors, no bindings): `background`, `foreground`, `accent`, `accent-foreground`, `border-color`, `focus-ring`, `radius` (`sm`/`md`/`lg`/`xl`). Names resolve against live design tokens on every rebuild (`finalizeWithTokens`).

Other common attributes: `variant`, `size` (control scale `default`/`sm`/`lg`/`icon`; on `text` also typography `heading`/`display`), `disabled`, `checked`, `selected`, `value`, `placeholder`, `icon`, `autofocus`, `role`, `label`, `expanded`, `key`, `global-key`.

### Identity

| Attribute | Scope |
| --- | --- |
| `key` | Sibling-scoped identity |
| `global-key` | Parent-independent identity (items that move between containers) |

Unkeyed same-kind siblings use positional identity; insert/remove earlier siblings can re-disambiguate trailing ones and hop engine-owned state (carets, scroll). Prefer keys on list rows.

## Structure tags

```html
<for each="visible" key="id" as="h">
  <row global-key="{h.id}">…</row>
</for>
<else>
  <text>Nothing yet</text>
</else>

<if test="{hasItems}">…</if>
<else>…</else>
```

- `<for>`: one or more children; `key` names an item field; every node an item emits shares that identity.
- `<else>` after `</for>`: empty iterable state.
- `<else>` after `</if>`: must immediately follow; nest for multi-branch (no `else-if` tag).
- Templates: top-of-file `<template name="…" args="…">` plus `<use template="…">`; optional `<slot/>` and `<import src="components/….native"/>`.
- Imports are relative to the root view directory; component files are templates-only. Compile multi-file docs with `canvas.CompiledMarkupImports(Model, Msg, "root.native", &sources)` and pass the same set on `MarkupOptions.sources`.

## Bindings

A path like `{h.streak}` resolves left to right from the model or a `for`/template scope:

| Source | Resolution |
| --- | --- |
| Model field | Direct read |
| Zero-arg `pub fn` inside `Model` | Called like a field |
| Arena scalar `pub fn (*const Model, Allocator)` | Formats into the one-build arena |
| `for each` | Slice/array field, `pub const` array, `fn (*const Model) []const T`, or arena-returning filter fn |
| Enums | Tag name for display and equality |

Everything bindable must live **inside** `pub const Model = struct { … }`. File-scope helpers next to the struct are invisible to bindings. Bindings are zero-argument; parameterized queries become named model methods.

Attribute values take a literal **or exactly one** `{expression}`. Text content interpolates any number of expressions (`{open} open · {done} done`). Path-only by design: message tags/payloads, `for each` iterables, import paths.

<Warning>
Binding a `canvas.TextBuffer` field directly on `text=` is a teaching error. Bind a `pub fn` that returns `.text()`, apply edits in `update`, and clear the buffer when the source should win.
</Warning>

## Expressions

Shared evaluator in `ui_markup_expr.zig` for the interpreter, compiled engine, and `native markup check`. Expressions are pure and total: closed function library, fixed bounds, guaranteed termination.

**Operands:** binding paths, numbers (no exponents), `'single-quoted strings'`, `true`/`false`.

**Operators:** arithmetic `+ - * /` (numbers only; `/` always float), comparison `== != < <= > >=` (no chaining), boolean `and`/`or`/`not`, concatenation `++`.

**Closed functions (17):** `fixed`, `thousands`, `percent`, `date`, `time`, `datetime`, `upper`, `lower`, `trim`, `min`, `max`, `abs`, `round`, `floor`, `ceil`, `plural`, `pad`.

**Bounds:**

| Limit | Value |
| --- | --- |
| Bytes | 256 |
| Terms/nodes | 64 |
| Nesting depth | 16 |
| Call args | 4 |

Division by zero, integer overflow, and non-finite floats are loud errors. `now()` is rejected—clock reads are effects; keep a timestamp field updated by `update`/`fx` and format with `date`/`time`/`datetime`. Prefer model methods for reused derivation; keep one-off presentation math inline (`{percent(done / total)}`).

## Messages and events

| Attribute | Payload shape | Notes |
| --- | --- | --- |
| `on-press`, `on-toggle`, `on-change`, `on-submit`, `on-dismiss`, `on-hold` | `tag` or `tag:{payload}` | Tag must be a `Msg` variant; payload coerces (int, float, enum tag, string, bool) |
| `on-input` | `Msg` with `canvas.TextInputEvent` | Every text edit |
| `on-scroll` | `Msg` with `canvas.ScrollState` | `scroll` only; echo offset through `value` |
| `on-resize` | `Msg` with `f32` | `split` only; echo fraction through `value` |
| `on-reach-end` | Plain `Msg` | Infinite-fetch hysteresis near content end |

Press rule: click lands on the nearest pressable widget under the pointer. Binding `on-press`/`on-toggle` makes any element a hit target. Nested pressables resolve to the deepest; text fields, scrolls, and modals claim their own presses. Value/text handlers are rejected on layout/decoration elements.

Context menus: declare `<context-menu>` as a **direct child** of the pressable parent. Children are `menu-item`s and `separator`s (with `if`/`for` around items). Attribute-less; presents as OS menu where available, else anchored canvas surface.

## Widget budgets

Fixed per-view capacities in `src/runtime/canvas_limits.zig`. Overflow is loud (`error.WidgetNodeLimitReached`, `WidgetContextMenuLimitReached`, `WidgetAnchoredSurfaceLimitReached`, chart limits, …); production degrades to the previous frame with a teaching diagnostic. Automation snapshots report headroom (`widget_nodes=N/1024`).

| Budget | Cap |
| --- | --- |
| Retained widget nodes / semantics | 1024 |
| Widget text bytes | 64 KiB |
| Context-menu items (view-wide) | 512 |
| Anchored floating surfaces | 16 |
| Chart series / points | 64 / 16384 |
| Spans per paragraph | 32 |

Bound unbounded collections: `virtualized` containers for model-held hundreds of rows; windowed `ui.virtualList` (Zig only) for dataset-scale lists; model windows + `on-scroll` / `on-reach-end` for non-uniform content.

## Validate and check

```bash
native markup check src/view.native   # grammar/structure, expressions, a11y, font tofu
native check                          # + model-contract binding/Msg validation when contract is built
zig build model-contract              # refresh zig-out/model-contract.zon from Model/Msg
```

`native markup check` reports `file:line:column` teaching errors without a full app build: unknown elements/attributes, expression bounds, unnamed interactive controls, role misuse, and literal codepoints outside the bundled face. With a fresh model contract (from `native test` or `zig build model-contract`), `native check` verifies binding paths, iterables, `key` fields, message tags, payload types, and expression types against the real `Model`/`Msg`, with did-you-mean. A stale/missing contract degrades to grammar-only checking with a loud note. Opt update-only names out of the unused-state lint with `pub const view_unbound = .{ … };` on `Model` or `Msg`.

Build-time: `CompiledMarkupView` turns markup/binding mistakes into `@compileError` with line/column. Runtime: hot-reload parse failures preserve the last good tree.

## Editor and LSP support

Tooling lives under `editors/native-markup/`.

| Piece | What it does |
| --- | --- |
| TextMate grammar | Tags, attributes, comments, `{…}` bindings, `on-*`, structure tags |
| `native markup lsp` | Diagnostics (same parser/validator as `markup check`), completion for elements/attributes, hover docs |
| VS Code extension | Dependency-free LSP client in `extension.js` (no `vscode-languageclient`, no npm build) |

LSP **does not** type-check binding paths or `Msg` tags against your app—that needs Model/Msg and runs at app build / hot reload / `native check` with a model contract.

### VS Code

```bash
zig build   # produces zig-out/bin/native
ln -s /path/to/native-sdk/editors/native-markup \
  ~/.vscode/extensions/native-sdk.native-markup-0.1.0
```

Optional setting when `native` is not on `PATH`:

```json
{ "native-markup.serverPath": "/path/to/native-sdk/zig-out/bin/native" }
```

Remove any `"files.associations"` mapping `*.native` → `html` so the `native-markup` language id applies. Scaffold-only HTML association is a fallback when the extension is not installed.

### Helix / Neovim

Helix: language-server command `native` with args `["markup", "lsp"]`, file-types `["native"]`. Neovim 0.10+: `vim.lsp.start` with `cmd = { "native", "markup", "lsp" }` on filetype `native-markup`.

### Smoke test without an editor

```bash
python3 editors/native-markup/scripts/lsp-smoke.py zig-out/bin/native
```

## Testing markup views

Unit tests build the tree without a display:

```zig
var view = try canvas.MarkupView(Model, Msg).init(arena, markup_source);
var ui = canvas.Ui(Msg).init(arena);
const tree = try ui.finalize(try view.build(&ui, &model));
// tree.msgForPointer / msgForKeyboard / msgForResize / msgForDismiss / …
```

Rebuild after every dispatch before pressing again. Disabled controls yield `null` from pointer helpers. Live verification: `native build -Dautomation=true`, then `native automate wait` / `widget-click` / `assert` against snapshot ids that match the structural ids unit tests see.

## Related pages

<CardGroup>
  <Card title="App model" href="/app-model">
    Model, Msg, update loop, hot reload boundaries, and binding without mutation.
  </Card>
  <Card title="State and data flow" href="/state-data-flow">
    Derive-don't-store, message dispatch, text editing paths, and effects.
  </Card>
  <Card title="Theming" href="/theming">
    Design tokens, live re-resolution, and token-only restyles.
  </Card>
  <Card title="Components" href="/components">
    Built-in catalog, previews, eject workflows, and widget identity.
  </Card>
  <Card title="Quickstart" href="/quickstart">
    native init, native dev, first .native edit, and a live window.
  </Card>
  <Card title="Agent skills" href="/agent-skills">
    Shipped skill packs including native-ui authoring guidance.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    native markup check, native check, and related verbs.
  </Card>
  <Card title="Automation" href="/automation">
    Snapshots, widget driving, and agent command surface over the live tree.
  </Card>
</CardGroup>
