# Components

> Built-in component catalog inventory, preview generation, eject workflows, and identity constraints for reusable widgets.

- 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

- `docs/src/lib/components-pages.ts`
- `docs/src/lib/component-vocab.json`
- `src/tooling/eject_components.zig`
- `docs/src/components/component-preview.tsx`
- `docs/src/components/eject-section.tsx`

---

---
title: "Components"
description: "Built-in component catalog inventory, preview generation, eject workflows, and identity constraints for reusable widgets."
---

The Native SDK ships a retained-canvas component catalog: each built-in is an engine-drawn widget (not a platform control skin), expressible as a `.native` markup element and as a `canvas.Ui` builder call. Docs inventory, attribute tables, static light/dark tiles, and the live wasm hover preview all bind to one generated vocabulary (`docs/src/lib/component-vocab.json`) and one scene catalog (`tools/docs_preview_scenes.zig`). Library **composites** that are honest compositions of public primitives can transfer into the app with `native eject component <name>`; engine control classes cannot.

## Ownership model

Three moves, in preference order:

| Move | When | Mechanism |
| --- | --- | --- |
| **Theme** | Restyle color, radius, type, control metrics, state washes | Design tokens / theme packs — see [Theming](/theming) |
| **Eject** | Own a composite’s **shape** (structure, order, extras) | `native eject component <name>` → `src/components/` once |
| **Build** | No library shape fits | Markup `<template>` / `<import>` / `<use>` / `<slot/>`, or a Zig view function |

Engine controls (buttons, text fields, tabs, and similar) are **not** ejectable: their behavior lives in the runtime. Change them with tokens, not a fork.

```text
  App markup / Zig view
          │
          ▼
  ┌───────────────────────┐     theme tokens      ┌──────────────────┐
  │ Built-in elements     │◄──────────────────────│ DesignTokens     │
  │ canvas.Ui builders    │                       └──────────────────┘
  └───────────┬───────────┘
              │ eject (composites only)
              ▼
  src/components/<name>.{native,zig}
  (app-owned; second eject → AlreadyEjected)
```

## Catalog inventory

Sidebar, index grid, titles, and OG metadata share one ordered list: `componentPages` in `docs/src/lib/components-pages.ts` (45 pages today). Each entry has `slug`, display `name`, preview tile stem (`preview` → `/components/<preview>-{light,dark}.webp`), and a one-line `blurb`.

| Slug | Preview stem | Blurb |
| --- | --- | --- |
| `accordion` | `accordion-hero` | Disclosure surface with a model-owned open state |
| `alert` | `alert-hero` | Inline callouts with icon and variant color |
| `avatar` | `avatar-hero` | Initials fallback and runtime-registered images |
| `badge` | `badge-hero` | Status labels in every variant |
| `breadcrumb` | `breadcrumb-hero` | Hierarchy trail with separators |
| `bubble` | `bubble-hero` | Chat-message surfaces for either side |
| `button` | `button-hero` | Variants, sizes, inline icons, and states |
| `button-group` | `button-group-hero` | Attached action buttons as one segmented bar |
| `card` | `card-hero` | The bordered, elevated surface container |
| `chart` | `chart-hero` | Line, bar, and band series (Zig builder) |
| `checkbox` | `checkbox-hero` | Binary choice with model-owned state |
| `combobox` | `combobox-hero` | Text entry with an anchored suggestions menu |
| `dialog` | `dialog-hero` | Modal surface with model-owned dismissal |
| `drawer` | `drawer-hero` | Side-anchored modal surface |
| `dropdown-menu` | `dropdown-menu-hero` | Anchored floating menus and menu items |
| `icon` | `icon-hero` | The built-in vector icon registry |
| `input` | `input-hero` | Single-line text entry: input, text field, search field |
| `input-group` | `input-group-hero` | One bordered field: textarea plus accessory actions |
| `list` | `list-hero` | Rows with icons, selection, and virtualization |
| `markdown` | `markdown-hero` | GFM rendering through native widgets |
| `pagination` | `pagination-hero` | Page navigation row |
| `panel` | `panel-hero` | The plain surface container |
| `progress` | `progress-hero` | Determinate progress bar |
| `radio` | `radio-hero` | Single choice within a radio group |
| `resizable` | `resizable-hero` | Panel with an engine-managed drag handle |
| `scroll` | `scroll-hero` | Scroll regions with model-observable offsets |
| `select` | `select-hero` | Trigger plus the anchored dropdown options pattern |
| `separator` | `separator-hero` | Hairline rules, horizontal and vertical |
| `sheet` | `sheet-hero` | Bottom-anchored modal surface |
| `skeleton` | `skeleton-hero` | Loading placeholders that sketch the content |
| `slider` | `slider-hero` | Continuous value control |
| `spacer` | `spacer-hero` | Flexible empty space between siblings |
| `spinner` | `spinner-hero` | Indeterminate progress leaf |
| `split` | `split-hero` | Draggable two-pane splitter |
| `status-bar` | `status-bar-hero` | Window-bottom status text |
| `stepper` | `stepper-hero` | Stage progress with completed/active/pending steps |
| `switch` | `switch-hero` | On/off switches with model-owned state |
| `table` | `table-hero` | Rows and cells with hairline dividers |
| `tabs` | `tabs-hero` | Tab strip over segmented controls |
| `textarea` | `textarea-hero` | Multi-line text entry |
| `timeline` | `timeline-hero` | Ledger list with indicators and connectors |
| `toggle` | `toggle-hero` | Pressed-state toggles, toggle buttons, and groups |
| `tooltip` | `tooltip-hero` | The floating label above the control it annotates |
| `tree` | `tree-hero` | Disclosure tree with one roving focus set |
| `virtual-list` | `virtual-list-hero` | Windowed rows: the view builds only what's visible |

Runtime catalog surface (engine): `native_sdk.canvas.builtin_component_kinds`, `builtin_component_names`, and `builtinComponentWidget(...)` construct each component’s default foundation (style, root widget kind, semantic role, composite flag).

### Markup element vocabulary

`component-vocab.json` is regenerated by `zig build docs-component-previews` (not hand-edited for docs tables). It currently exposes on the order of **65** markup elements plus structure tags `for`, `if`, `else`, `template`, `use`, `import`, and `slot`. Representative families:

| Family | Element names (examples) |
| --- | --- |
| Layout | `row`, `column`, `stack`, `panel`, `card`, `scroll`, `list`, `grid`, `split`, `tree`, `spacer`, `separator` |
| Text / chrome | `text`, `badge`, `status-bar`, `tooltip`, `span` |
| Controls | `button`, `checkbox`, `radio`, `toggle`, `switch`, `toggle-button`, `slider`, `progress`, `text-field`, `search-field`, `textarea`, `input`, `select`, `combobox` |
| Grouping | `button-group`, `radio-group`, `toggle-group`, `tabs`, `breadcrumb`, `pagination`, `input-group`, `input-group-actions` |
| Surfaces | `accordion`, `alert`, `bubble`, `dialog`, `drawer`, `sheet`, `dropdown-menu`, `context-menu`, `resizable` |
| Data / media | `table` / `table-row` / `table-cell`, `avatar`, `icon`, `markdown`, `chart` / `series`, `skeleton`, `spinner` |
| Composites | `stepper` / `step`, `timeline` / `timeline-item`, `reactions` (bubble child) |

Full attribute semantics, flex layout, bindings, and expressions live under [Native UI markup](/native-ui). Attribute tables on component docs resolve names against this vocab at docs build time and throw if a name is missing (regenerate previews).

## Dual authoring surface

Every catalog component is intended to be expressible two ways:

**Markup** (declarative, hot-reloadable in `native dev`):

```html
<stepper active="{publish_step}">
  <step>Draft</step>
  <step>Review</step>
  <step>Publish</step>
</stepper>
```

**Zig builder** (when the closed markup grammar is not enough — image ids, per-cell templates, floating surfaces, or structure that needs typed messages):

```zig
ui.stepper(.{ .active = model.publish_step }, &.{
    .{ .label = "Draft" },
    .{ .label = "Review" },
    .{ .label = "Publish" },
});
```

State remains model-owned: bindings and message handlers wire through the TEA loop; markup does not mutate state in place. See [App model](/app-model) and [State and data flow](/state-data-flow).

## Preview generation

### Static tiles (docs site)

```bash
zig build docs-component-previews
```

| Output | Path / shape |
| --- | --- |
| WebP pairs | `docs/public/components/<scene>-{light,dark}.webp` |
| Icon gallery | `docs/public/components/icons/<icon>-{light,dark}.webp` |
| Vocab JSON | `docs/src/lib/component-vocab.json` (elements, attributes, tokens, `previews` dimensions, `ejectable`) |

Implementation: `tools/docs_component_previews.zig` drives each scene from `tools/docs_preview_scenes.zig` through the **deterministic reference renderer** (offscreen `TestHarness`, fixed layout/emit, no platform fonts). PNG cache lands under `/tmp/native-sdk-component-previews`; conversion uses `cwebp` lossless (stable bytes per cwebp release). Light and dark schemes use `DesignTokens.theme(.{ .color_scheme = ... })`. Hover states are optional per scene via pointer_move into a target widget frame.

Index cards use the `*-hero` stem from `componentPages.preview`. Per-page `<ComponentPreview name="..." />` looks up `vocab.previews[name]` for width/height and fails the docs build on unknown names.

### Live wasm upgrade

```bash
zig build docs-wasm-preview
# → docs/public/wasm/component-preview.wasm
```

In the browser, `ComponentPreview` starts as the static light/dark pair and upgrades on hover/click/focus to the **same scene catalog** compiled to `wasm32-freestanding`. The host pumps `gpu_surface_input`, reads pixels from the CPU reference renderer, and follows the site theme. Effects do not run in the preview host; interactivity is engine control state plus each scene’s mini-model (same dispatch shape as a UiApp, scaled to the catalog).

```mermaid
flowchart LR
  subgraph scenes [docs_preview_scenes.zig]
    S[Scene build + model]
  end
  subgraph static [docs-component-previews]
    R[TestHarness + layout + emit]
    W[cwebp light/dark WebP]
    V[component-vocab.json]
  end
  subgraph live [docs-wasm-preview]
    M[component-preview.wasm]
    C[Browser canvas / rAF]
  end
  S --> R --> W
  S --> M --> C
  R --> V
  V --> Docs[ComponentPreview / AttrTable / EjectSection]
```

### Docs integrity constraints

- Unknown preview name in MDX → build error: regenerate with `zig build docs-component-previews`.
- Unknown ejectable name in `<EjectSection>` → build error against `vocab.ejectable` (same rows as the CLI registry).
- `previews` map dimensions must match rendered scenes so tiles do not ship with wrong aspect boxes.

## Eject workflow

### CLI

```bash
native eject component <name> [dir]
```

| Item | Detail |
| --- | --- |
| **Requires** | `app.zon` in the app directory (or pass `[dir]`) |
| **Registry** | `src/tooling/eject_components.zig` → `components` table |
| **Creates** | Parent `src/components/` if needed; writes embedded canonical source |
| **Once** | Destination already present → `AlreadyEjected` (“delete it to re-eject”) |
| **Typos** | Edit distance ≤ 2 against ejectable names; else lists all ejectable names |
| **Success** | Prints path + form; header in the file documents call-site migration; run `native check` |

Not the same verb as plain `native eject` (build-graph ownership of `build.zig` / `build.zig.zon`). `eject component` is dispatched first so `component` is never treated as a directory.

### Ejectable set (current)

| Name | Form | Destination |
| --- | --- | --- |
| `stepper` | Zig view function | `src/components/stepper.zig` |
| `timeline` | Markup template | `src/components/timeline.native` |
| `timeline-item` | Zig view function | `src/components/timeline_item.zig` |

Growing the set is three steps: add canonical source under `src/tooling/components/`, add identity tests, add a row to `eject_components.components`. Canonical sources are `@embedFile`’d into the CLI binary.

### After eject: migrate call sites

Eject transfers **ownership**, not a new element name. Markup templates keep working only through `<use>`; the built-in element still means the library form at unmigrated sites.

**Timeline container** (template):

```html
<!-- before -->
<timeline gap="4" label="Activity">...</timeline>

<!-- after -->
<import src="components/timeline.native"/>
...
<use template="timeline" gap="4" label="Activity">...</use>
```

`timeline-item` children still flow into the template’s `<slot/>`. Zig ejects import as ordinary Zig modules and call `build` / typed helpers as documented in each file header.

Every ejected file opens with an ownership header containing `Ejected from the Native SDK component library` and the restoring command `native eject component ...` (enforced by unit tests).

### Docs mirror

Component MDX pages that are ejectable render `<EjectSection components={[...]} />`, which resolves each name against `vocab.ejectable` and prints the matching `native eject component …` lines so docs never advertise an eject the CLI refuses.

## Identity constraints

Identity tests (`src/tooling/components/identity_tests.zig`, suite step `zig build test-eject-components`, also under `zig build test`) prove:

1. **Tree equality** — For shared inputs, the ejected form and the library form produce identical widget trees: every id, field, and handler (`expectEqualDeep` on root and handlers).
2. **Behavior** — Press handlers and other msg bindings fire the same payloads (for example timeline-item `on_press`).
3. **Markup templates** — Ejected `<use template="timeline">` (via import of the embedded template) matches the built-in `<timeline>` element tree for the same children; ids hash as if the body were written in place.
4. **Write-once** — `eject` succeeds once and returns `AlreadyEjected` on a second write with unchanged contents.

Consequence: ejecting is never a visual or behavioral change at the moment of transfer—only an ownership change. A library refactor that drifts a composite’s tree fails identity tests until the canonical source is updated.

Widget identity rules that affect all components (not only eject):

- Template expansion inlines bodies so **ids hash as handwritten markup** (no identity jump when extracting a template).
- `key` is sibling-scoped; `global-key` survives reparenting.
- Automation and accessibility targets depend on stable structure and labels—forked composites must preserve or deliberately change those contracts.

## App-authored components

When building new shapes:

| Form | File convention | Reach |
| --- | --- | --- |
| Markup component file | Templates only under markup root (tooling convention: `src/components/*.native`) | `<import src="…"/>` then `<use template="…">` |
| Zig view function | Ordinary Zig module | Call from any Zig view; compose with markup windows as needed |

Rules that matter in practice:

- Style through **token references** (`background="surface"`, `style_tokens = .{ .background = .surface }`), not hardcoded colors—components inherit app retheme for free.
- `native markup check` / `native check` walk the import closure; component files also check standalone.
- Runtime and compiled engines share one embedded `SourceFile` set for markup imports; add each component file once for production and tests.
- Markup grammar is closed: image ids, per-cell render callbacks, and some floating surfaces remain Zig-side (see [Native UI markup](/native-ui)).

## Commands and verification

| Goal | Command / signal |
| --- | --- |
| Regenerate docs tiles + vocab | `zig build docs-component-previews` → webp count log + vocab path |
| Regenerate live preview wasm | `zig build docs-wasm-preview` |
| Eject a composite | `native eject component stepper` (cwd with `app.zon`) |
| Validate app after eject | `native check` |
| Identity suite | `zig build test-eject-components` or `zig build test` |
| Docs build guard | Unknown `ComponentPreview` / `EjectSection` / `AttrTable` name fails the Next/docs build |

### Failure modes

| Symptom | Cause | Action |
| --- | --- | --- |
| `unknown component "…"` | Name not in eject registry | Use `stepper`, `timeline`, or `timeline-item`; heed did-you-mean |
| `already ejected at …` | Destination file exists | Edit the app copy, or delete to re-eject a fresh library snapshot |
| `no app.zon here` | Wrong directory | Run inside the app root or pass `[dir]` |
| Docs: unknown preview / attribute / ejectable | Vocab stale or typo | `zig build docs-component-previews` |
| Identity test fails after SDK edit | Canonical source drifted from library builder | Update `src/tooling/components/*` to match library trees |

## Related pages

<CardGroup>
  <Card title="Native UI markup" href="/native-ui">
    Elements, attributes, flex layout, templates, imports, slots, and LSP/check semantics for `.native` views.
  </Card>
  <Card title="Theming" href="/theming">
    Design tokens, live re-resolution, and token-only restyles across the catalog without ejecting.
  </Card>
  <Card title="App model" href="/app-model">
    Model, Msg, and update wiring—how component bindings and handlers attach without mutating state in markup.
  </Card>
  <Card title="State and data flow" href="/state-data-flow">
    Derive-don’t-store, message dispatch, text editing paths, and effects as the side-effect channel.
  </Card>
  <Card title="CLI reference" href="/cli-reference">
    `native eject component`, `check`, `dev`, and related flags, outputs, and failure modes.
  </Card>
  <Card title="Automation" href="/automation">
    Accessibility snapshots and widget driving against the same retained widget trees components produce.
  </Card>
</CardGroup>
