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