CLI Reference #
The webui command-line tool is the primary way to build WebUI applications. It takes your app folder containing HTML templates and web components, and produces the WebUI protocol output ready for server-side rendering.
Installation #
Install via npm:
npm install @microsoft/webui
Or install via Cargo for standalone CLI use:
cargo install microsoft-webui-cli
Commands #
Global options #
These flags work with any command:
| Option | Description | Default |
|---|---|---|
--format <FORMAT> | Output format: human (colorized terminal) or json (machine-readable diagnostics on stdout) | human |
Use --format json in editors, CI, or AI/agent tooling that needs to parse build errors programmatically instead of scraping colorized terminal text. See Error output and exit codes.
webui build #
Build a WebUI application from an app folder.
webui build [APP] --out <OUT> [--entry <FILE>] [--css <MODE>] [--dom <MODE>] [--css-bundle] [--plugin <NAME>] [--components <SOURCE>]... [--projection-manifest <PATH>]... [--emit-component-assets <TAGS>] [--metafile <PATH>] [--theme <VALUE>] [--asset-file-name-template <TEMPLATE>] [--css-public-base <BASE>] [--legal-comments <MODE>]
Arguments:
| Argument | Description | Default |
|---|---|---|
APP | Path to the app folder | . (current directory) |
--out <OUT> | Output folder for protocol and assets, or a .bin file path to set the protocol filename (e.g. ./dist/app1.bin) | (required) |
--entry <FILE> | Entry HTML file name | index.html |
--css <STRATEGY> | CSS delivery strategy: link, style, or module | link |
--dom <MODE> | Fallback for components without an authored Shadow root: shadow or light | shadow |
--css-bundle | Merge component stylesheets into shared chunks. Composes with --css; rejected with --css module. | (off) |
--plugin <NAME> | Load a parser plugin | (none) |
--components <SOURCE> | Additional component sources (npm packages or local paths). Repeatable. | (none) |
--projection-manifest <PATH> | Bundler projection manifest fragment. Repeatable and valid only with --plugin=webui. | (none; full state) |
--emit-component-assets <TAGS> | Comma-separated root component tags to emit as static WebUI component assets in --out | (none) |
--metafile <PATH> | Write an esbuild-compatible component asset graph. Requires --emit-component-assets. | (none) |
--theme <VALUE> | Design token theme to validate against: a JSON file path or npm package name. Missing required tokens fail the build. | (none) |
--asset-file-name-template <TEMPLATE> | Emitted asset filename template for Link-mode CSS files and static component assets. Tokens: [name], [hash], [ext] | [name].[ext] |
--css-public-base <BASE> | Optional public URL/path prefix for Link-mode CSS hrefs | (none) |
--legal-comments <MODE> | Legal comment handling: inline preserves legal CSS comments, none strips all comments | inline |
Path inputs for APP, --state, --servedir, --projection-manifest, and
--metafile support absolute paths, relative paths, ~/..., and file://...
URI-style values.
CSS Modes:
| Mode | Behavior |
|---|---|
link | Emits external .css files and installs their <link> resources in compiler-defined cascade order. |
style | Installs compiled CSS in <style> elements. No separate CSS files are written. |
module | Delivers compiled CSS with an SSR fallback and shares imported CSS module stylesheets across component instances when supported. No separate CSS files are written. |
All modes support Light and Shadow components. A component's ordinary paired
CSS file remains authored/global CSS in Light DOM and remains native Shadow CSS
in Shadow DOM. Resources are installed once per Document or ShadowRoot in
first-discovery order, including partial navigation, streaming, and static
component assets. Full-document SSR installs Document resources before
</head>. When the document omits an explicit head, resources precede document
content while remaining immediately after any leading doctype.
Document fragment renders install resources before fragment content; a Shadow
component rendered directly as the entry installs them inside
its declarative root.
For long-lived CDN/browser caching, include [hash] in
--asset-file-name-template. [hash] is the emitted file's SHA-256 content hash
truncated to 8 hex characters. Link-mode CSS files are still written to --out;
--css-public-base only changes the CSS href stored in protocol.bin and
emitted in <link> tags. Templates must be ASCII filenames. URL delimiters
(#, %, and ?), path separators, whitespace, control characters, and
Windows-reserved filename characters are rejected.
CSS bundling:
Every component stylesheet is render-blocking, so one file per component costs a
request each and forfeits cross-file compression. --css-bundle merges component
stylesheets into shared chunks:
webui build ./my-app --out ./dist --css link --css-bundle
It composes with --css rather than replacing it: bundling decides how
stylesheets are grouped, --css decides how they reach the page. A Link
build gets fewer <link> tags and requests, and a Style build gets fewer inline
blocks.
Chunks split rather than duplicate. Only components reached by an identical set of CSS trees share a chunk, so a stylesheet used by several routes lands in its own chunk and is downloaded and cached once instead of being copied into every route bundle. Cascade order is preserved exactly: a chunk's members must be adjacent and identically ordered in every closure that contains them. The compiler verifies both properties and splits any incompatible chunk.
Chunks are named _chunk-<first-member>-<count>, or the component's own tag when
a chunk has a single member. The leading underscore keeps multi-member resource
IDs distinct from legal component tags. Link builds retain per-component files
as independently loaded component and older-handler fallbacks, but current
handlers link only chunks on the bundled path, so the fallbacks add no requests.
Pair bundling with content-hashed filenames so chunks can be served immutably:
webui build ./my-app --out ./dist --css link --css-bundle \
--asset-file-name-template "[name]-[hash].[ext]"
The default template is [name].[ext], which emits _chunk-nav-4.css. That name
is stable across builds even when the CSS inside it changes, so it cannot carry a
long Cache-Control: max-age=โฆ, immutable. With [hash] the same chunk becomes
_chunk-nav-4-36c58ce5.css and changes only when its bytes change, which is what
makes a shared chunk worth sharing: it stays in cache across deploys and across
routes.
Measured on a 26-component example over HTTP/2 with Brotli, bundling is a byte
and CSSOM optimization first: 14% fewer compressed CSS bytes (identical rules
compress better in fewer, larger files) and 27% fewer CSSStyleSheet objects,
both deterministic. Load-time metrics improve by low single-digit percentages.
The win is substantially larger over HTTP/1.1, where request count is bounded by
head-of-line blocking rather than multiplexed.
--css-bundle is rejected with --css module, which already inlines every
stylesheet as a data URI: there is no request to merge, and module specifiers are
resolved per component at compile time. Bundling is off by default, so protocol
size and emitted resource names are unchanged unless you opt in.
Component assets:
Use --emit-component-assets with the WebUI plugin to prebuild CDN-loadable
template assets for deferred UI such as dialogs loaded without
@microsoft/webui-router:
webui build ./my-app --out ./dist --plugin=webui \
--emit-component-assets mail-thread,compose-page \
--metafile ./dist/component-assets-meta.json
The flag is a strict comma-separated allowlist. Every tag must be a discovered
lowercase kebab-case component. Requested roots are compiled through synthetic
non-entry fragments, so they do not become part of initial SSR unless your entry
template also references them. A build containing both component assets and a
<route> fails with component-assets-with-routes; use the router's normal
partial-navigation pipeline for routed components.
Assets are ESM graph modules. Entry-reachable components stay in protocol.bin
and the application bundle, and become external prerequisites instead of being
copied. A dependency used by one asset root stays inline in that root.
Dependencies shared by the same two or more roots are emitted once as
chunk-<first-sorted-component>.webui.js, and each root dynamically imports the
chunks it needs. Requested-root order does not change ownership, bytes, or
hashes. Asset-only records are removed from protocol.bin.
Component assets use version 3 with a required, atomically registered
componentStyles catalog. Other versions and assets without the catalog are
rejected before registration.
--metafile writes esbuild-compatible inputs and outputs, including every
root-to-chunk dynamic-import edge and exact byte attribution. It can be opened
directly in an esbuild bundle analyzer or consumed by build tooling. The
metafile path is collision-checked with protocol, CSS, root, and chunk outputs
before any files are written.
FAST plugin builds can emit the same graph with <f-template>
payloads, but need a FAST-owned runtime loader. Every module intentionally omits
inventory state because a static CDN asset cannot know the page's loaded
template bitset. Use --asset-file-name-template "[name]-[hash].[ext]" for
long-lived CDN caching; [hash] is each module's SHA-256 content hash truncated
to 8 hex characters.
Load an asset before creating the component:
import { mailAssets } from './lazy-assets.js';
mailAssets.preload('mail-thread');
panelSlot.replaceChildren(await mailAssets.create('mail-thread'));
// lazy-assets.ts
import { defineComponentAssets } from '@microsoft/webui-framework/component-asset.js';
export const mailAssets = defineComponentAssets({
'mail-thread': {
asset: '/mail-thread.webui.js',
module: () => import('./mail-thread/mail-thread.js'),
data: async () => await (await fetch('/mail-thread-data.json')).json(),
},
});
Keep the lazy component tag out of SSR-reachable templates unless it should be
eligible for initial SSR. Use a mount element or another non-HTML trigger, then
create the custom element with mailAssets.create(...). The application must
load its normal entry bundle before component assets because entry-reachable
dependencies are external prerequisites. For Shadow builds, the compiler records final Link stylesheet hrefs in the
protocol so preload(tag) can start CSS beside the authored stable root asset
without exposing content-hashed stylesheet names. Light builds emit those hrefs
as document stylesheets with the entry because their CSS is globally scoped.
Comment handling:
WebUI strips HTML comments and CSS comments at build time. Bindings or
directives inside HTML comments are ignored and never produce fragments or
hydration metadata. Inside <style> tags, dynamic CSS fragments are valid only
when wrapped as exact CSS block comments, such as /*{{{tokens.light}}}*/.
With the default --legal-comments inline, CSS comments that contain
@license or @preserve, or start with /*! or //!, are preserved inline.
Use --legal-comments none to strip all non-signal comments.
Component DOM ownership:
Shadow is the backward-compatible default: unwrapped component content receives
a compiler-generated open Shadow root. Pass --dom light to render unwrapped
components as direct Light DOM children with authored/global CSS in their
owning CSS tree. Light CSS is not selector-rewritten or marker-scoped, so
ordinary selectors can reach other Light DOM in that tree. In either build mode,
a sole bare top-level <template> explicitly selects Light and is unwrapped.
A sole top-level <template shadowrootmode="open"> is authoritative and keeps
that component Shadow, so either build can contain explicit Shadow islands.
Templates with attributes and policy wrappers such as w-render remain
ordinary/policy content rather than selecting a mode.
:host, :host(...), :host-context(...), and ::slotted(...) are Shadow-only
and fail with unsupported-light-css in effective Light CSS. Use ordinary
selectors such as the component tag, or author an open Shadow root.
Closed roots and invalid values or placement always fail the build. Native
<slot> is allowed in effective Shadow components and rejected in effective
Light components.
FAST 2/3 plugins currently require effective Shadow components. Combining
--plugin fast, fast-v2, or fast-v3 with an effective Light component
fails with fast-light-dom-unsupported instead of allowing the FAST client
runtime to replace Light SSR with a Shadow root.
See Performance - Light DOM vs Shadow DOM for benchmarks and guidance.
Examples:
# Build from current directory
webui build --out ./dist
# Build a specific app folder
webui build ./my-app --out ./dist
# Use a custom entry file
webui build ./my-app --out ./dist --entry home.html
# Opt into mixed Light DOM with authored Shadow islands
webui build ./my-app --out ./dist --dom light
# Build with style CSS (no external CSS files)
webui build ./my-app --out ./dist --css style
# Build link-mode CSS with content-hashed filenames
webui build ./my-app --out ./dist --asset-file-name-template "[name]-[hash].[ext]"
# Point generated stylesheet hrefs at a CDN/public asset root
webui build ./my-app --out ./dist \
--asset-file-name-template "[name]-[hash].[ext]" \
--css-public-base "https://cdn.example.com/assets"
# Build with the WebUI Framework plugin (hydration support)
webui build ./my-app --out ./dist --plugin=webui
# Build browser code first, then embed exact state projection metadata
node ./my-app/build-client.mjs
webui build ./my-app --out ./dist --plugin=webui \
--projection-manifest ./my-app/dist/webui-projection.json
# Build with external component packages
webui build ./my-app --out ./dist --components @reactive-ui
# Validate CSS design tokens against a theme
webui build ./my-app --out ./dist --theme ./themes/brand.json
# Build with components from a local shared library
webui build ./my-app --out ./dist --components ./shared/components
# Customize the protocol filename (useful when building multiple apps to one folder)
webui build ./src/apps/app1 --out ./dist/app1.bin
webui build ./src/apps/app2 --out ./dist/app2.bin
--projection-manifest is opt-in and strict. Without it, WebUI performs no
JavaScript analysis and preserves full state. With one or more fragments, every
scripted component compiled from the app or --components sources must have
exactly one manifest entry. Build external component bundles separately and
repeat the flag for each fragment. See
Build-Time State Projection.
webui inspect #
Inspect a protocol.bin file by converting it to JSON and printing to stdout. Useful for debugging and piping to tools like jq.
webui inspect <FILE>
Arguments:
| Argument | Description |
|---|---|
FILE | Path to a protocol.bin file |
Examples:
# Inspect a protocol file
webui inspect dist/protocol.bin
# Pretty-print a specific fragment with jq
webui inspect dist/protocol.bin | jq '.fragments["index.html"]'
# Count total fragments
webui inspect dist/protocol.bin | jq '.fragments | keys | length'
webui serve #
Start a development server that builds, renders, and serves a WebUI application. Enable live reload with --watch.
webui serve [APP] --state <FILE> [--servedir <DIR>] [--watch] [--port <PORT>] [--entry <FILE>] [--css <MODE>] [--dom <MODE>] [--css-bundle] [--plugin <NAME>] [--components <SOURCE>]... [--projection-manifest <PATH>]... [--api-port <PORT>] [--emit-component-assets <TAGS>] [--metafile <PATH>] [--theme <VALUE>] [--asset-file-name-template <TEMPLATE>] [--css-public-base <BASE>] [--legal-comments <MODE>]
Arguments:
| Argument | Description | Default |
|---|---|---|
APP | Path to the template/component directory | . (current directory) |
--state <FILE> | Path to JSON state file for rendering | (required) |
--servedir <DIR> | Directory served at /* | (optional) |
--watch | Enable file watching + HMR | false |
--port <PORT> | Port to bind the development server | 3000 |
--entry <FILE> | Entry HTML file name | index.html |
--css <MODE> | CSS delivery strategy: link, style, or module | link |
--dom <MODE> | Fallback for components without an authored Shadow root: shadow or light | shadow |
--css-bundle | Merge component stylesheets into shared chunks. Composes with --css; rejected with --css module. | (off) |
--plugin <NAME> | Load parser + handler plugins (e.g., webui) | (none) |
--components <SOURCE> | Additional component sources (npm packages or local paths). Repeatable. | (none) |
--projection-manifest <PATH> | Bundler projection manifest fragment. Repeatable and valid only with --plugin=webui. | (none; full state) |
--api-port <PORT> | Proxy route requests to your API server. JSON responses provide buffered state; application/x-webui-stream responses drive progressive boundary rendering. Encoded paths and queries are forwarded unchanged. | (none) |
--emit-component-assets <TAGS> | Comma-separated root component tags to compile as static WebUI component assets, matching webui build. Their templates and CSS are parsed and validated on every build, and the compiled <tag>.webui.js modules are served from memory. | (none) |
--metafile <PATH> | Atomically replace an esbuild-compatible component asset graph after each successful build. Requires --emit-component-assets. | (none) |
--theme <VALUE> | Design token theme: a path to a JSON file or an npm package name. Missing required tokens fail the build; resolved tokens are injected into the render state. | (none) |
--asset-file-name-template <TEMPLATE> | Emitted asset filename template for Link-mode CSS files. Tokens: [name], [hash], [ext] | [name].[ext] |
--css-public-base <BASE> | Optional public URL/path prefix for Link-mode CSS hrefs | (none) |
--legal-comments <MODE> | Legal comment handling: inline preserves legal CSS comments, none strips all comments | inline |
The APP directory should contain your entry HTML and component files.
What it does:
- Builds the protocol from your
APPdirectory (no separatewebui buildstep needed) - Renders the entry template with state data
- Serves the rendered HTML with an injected live-reload script
- If
--watchis enabled, watches app, state, asset, and explicit projection manifest files for changes - If
--watchis enabled, automatically rebuilds and re-renders when files change - If
--watchis enabled, connected browsers reload automatically via the polling HMR backend
When --api-port is set, backend state requests and /api/* forwarding use
the encoded path and query exactly as received except for the entry route alias.
/ and /index.html both resolve backend state at / (the entry path is
normalized), while still preserving the query string. All other request paths
forward their encoded path and query unchanged. Do not double-encode route
parameters for development. For example, %2F remains part of one parameter
instead of becoming a path separator.
For progressive HTML, the server sends
Accept: application/x-webui-stream, application/json to the API backend. A
backend can return a versioned NDJSON control stream:
{"type":"start","version":2,"state":{"query":""}}
{"type":"resume","boundary":{"owner":"ntp-page","name":"search-ready"},"state":{"query":""},"mode":"updatable"}
{"type":"update","boundary":{"owner":"ntp-page","name":"search-ready"},"state":{"query":"webui"}}
start appears once. It renders until the first runtime occurrence or terminal.
Each resume.boundary must match the descriptor currently returned by WebUI
using owner, name, and key; omit key only when that descriptor has none.
An optional declarationId can tighten the match. Resume state is passed to
that occurrence and mode is final by default or updatable.
update.boundary uses the same identity to target one previously committed
updatable occurrence and requires object-valued state.
The control stream has no advance record because the CLI drives that core
operation:
| Core step state | CLI action |
|---|---|
| descriptor present | Wait for the matching resume control and call core resume |
| no descriptor and not done | Call core advance |
| done | Complete the browser response |
Core resume emits only the pending occurrence through its checkpoint. Core
advance emits the following parent or tail bytes through the next descriptor
or terminal. After the backend sends the resume for the final descriptor and
closes its NDJSON body, the CLI's final advance emits the terminal. There is
no separate end command.
The CLI owns response-local instance IDs and the browser transport. A
capacity-one command channel preserves backpressure, and each record is capped
at 2,000,000 bytes. Before HTTP 200, bytes from start are staged without
copying up to a 4,000,000-byte limit. Dropping the browser response cancels the
backend stream. The backend must honor its HTTP writer's backpressure signal and
cap concurrent streams. Returning JSON retains ordinary buffered behavior. See
<boundary> and
examples/app/streaming.
If the backend is unreachable, returns state the server cannot parse, or answers
a stream request with a non-success status such as 503 from its concurrency
cap, webui serve logs one warning and still renders the page from fallback
state. A refused request never started a stream, so it degrades the same way an
unreachable backend does instead of replacing your app with the upstream error
body. A failure that occurs after the stream is live still fails the response,
because bytes already sent to the browser cannot be rewound.
After generated assets and --servedir files miss, route fallback is based on
the Accept header. Requests that explicitly accept text/html or
application/xhtml+xml receive the SSR document, and requests that explicitly
accept application/json receive the JSON partial response. q=0 disables
that media type, while a malformed or out-of-range q value falls back to
q=1.0; when HTML and JSON are both acceptable, the higher q wins and exact
ties prefer JSON. Missing or wildcard-only Accept headers return 404, as do JS,
CSS, image, and other
non-HTML/non-JSON asset requests. Dots are valid in route segments, so paths
such as /docs/v2.1 can still fall back to the route renderer.
Examples:
# Start serving the current directory
webui serve . --state ./state.json --servedir ./assets
# Start serving a specific templates directory
webui serve ./examples/app/hello-world/templates --state ./examples/app/hello-world/data/state.json --servedir ./examples/app/hello-world/assets --watch
# Use a custom port
webui serve ./my-app --state ./state.json --servedir ./assets --port 9090 --watch
# Use style CSS mode
webui serve ./my-app --state ./state.json --servedir ./assets --css style --watch
# Use the WebUI Framework plugin for hydration
webui serve ./my-app --state ./state.json --plugin=webui --port 3001
# Rebuild when the client bundler atomically replaces its manifest
webui serve ./my-app --state ./state.json --plugin=webui \
--projection-manifest ./dist/webui-projection.json --watch
# Dev server with external components (--watch watches local paths)
webui serve ./my-app --state ./state.json --components @reactive-ui --watch
# Proxy route requests to your API server (e.g. Express on port 4000)
webui serve ./my-app --state ./state.json --api-port 4000 --watch
# Apply a design token theme from an npm package
webui serve ./my-app --state ./state.json --theme @my-org/brand-tokens --watch
# Apply a design token theme from a local JSON file
webui serve ./my-app --state ./state.json --theme ./themes/dark.json --watch
When --theme is present on build or serve, every required token must
exist in every theme. Nested fallback tokens are validated individually:
var(--a, var(--b, var(--c))) requires a, b, and c unless a token is
defined by local or ancestor CSS. A var() usage with a literal fallback (e.g.
var(--brand, #000)) is exempt โ the token is still hoisted for runtime
resolution but its absence does not fail the build. When such a literal-fallback
token is also absent from every theme it is surfaced as a non-fatal
unthemed-token warning (rendered like an error, with location, snippet, and
a did you mean โฆ? suggestion) since it is usually a typo.
--emit-component-assets behaves identically on serve and build: each listed
root is parsed and validated on every build - its template and CSS are checked
for HTML and theme-token errors even though the component is not part of the
initial SSR tree - so authoring mistakes in lazily loaded components fail the
dev build instead of being silently skipped. Root and shared chunk modules are
served from memory (and rebuilt on change under --watch), so no separate
webui build step or --out directory is needed during development. With
--metafile, a successful rebuild atomically replaces the graph; a failed
rebuild leaves the last valid metafile untouched. The metafile itself is ignored
by the watcher to prevent rebuild loops.
In serve --watch, rebuild failures are sticky: the terminal and live-reload
SSE report the error, and refreshing the page returns the latest rebuild error
instead of stale HTML while keeping the live-reload connection active. The next
successful rebuild clears the error and reloads connected browsers.
Routes:
| Path | Description |
|---|---|
/ or /index.html | Rendered HTML with live-reload script |
/*.webui.js | In-memory root and shared component assets emitted by --emit-component-assets |
/* | Static files from --servedir (when provided) |
/* with Accept: text/html, application/xhtml+xml, or application/json at q > 0 after asset misses | SPA route fallback (highest q wins; JSON wins exact ties) |
| Missing JS, CSS, image, and wildcard-only asset requests | 404 |
/hmr | HMR version endpoint (polling backend, only when --watch) |
Error output and exit codes #
When a template has an authoring mistake, the CLI prints a structured diagnostic with a stable error code, the source location, the offending snippet, and an actionable help: line:
โ error: invalid <for> each expression [invalid-for-each]
--> index.html:67:5
each="person inpeople"
help: use the form each="item in collection", e.g. each="todo in todos"
Where the mistake is likely a typo, the help: line suggests the intended name โ a misspelled directive attribute (eahc โ each) or an unregistered custom-element tag that closely matches a registered component in the same namespace (<mp-buton> โ <mp-button>). A custom element in a different namespace (e.g. a third-party <md-button>) is left untouched and passes through to the browser.
JSON diagnostics #
With --format json, each error is emitted as a single JSON object on stdout (the colorized terminal output is suppressed), so editors, CI, and AI assistants can consume it directly:
webui build ./my-app --out ./dist --format json
{
"severity": "error",
"code": "invalid-for-each",
"message": "invalid <for> each expression",
"file": "index.html",
"line": 67,
"column": 5,
"snippet": "each=\"person inpeople\"",
"help": "use the form each=\"item in collection\", e.g. each=\"todo in todos\"",
"chain": ["Build failed", "Failed to parse index.html", "..."]
}
Fields that don't apply to a given error are null. The code is stable across releases โ branch on it rather than on the human-readable message.
Exit codes #
The process exit code follows the BSD sysexits.h conventions so scripts and CI can branch on the cause:
| Code | Meaning |
|---|---|
0 | Success |
1 | Generic failure |
2 | Invalid arguments / usage |
65 | Template or authoring error (EX_DATAERR) |
66 | Missing input: app folder, --state file, --servedir, or entry file (EX_NOINPUT) |
69 | Requested --port is already in use (EX_UNAVAILABLE) |
74 | I/O error reading or writing files (EX_IOERR) |
App Folder Structure #
The CLI expects your app folder to contain an entry HTML file and optionally web component files:
my-app/
โโโ index.html # Entry template (or specify with --entry)
โโโ my-card.html # Web component: <my-card>
โโโ my-card.css # Component styles (auto-discovered)
โโโ nav-bar.html # Web component: <nav-bar>
โโโ nav-bar.css # Component styles
โโโ styles.css # Global styles
โโโ app.js # Client-side scripts
Component Discovery #
The CLI automatically discovers web components in your app folder:
- HTML files with a hyphen in the name are treated as components (e.g.,
my-card.htmlโ<my-card>) - CSS files with the same name are automatically paired (e.g.,
my-card.css) - Components are registered and available for use in your templates
- Discovery is recursive - components in subdirectories are also found
Entry Template #
Your entry HTML file is a standard HTML document using WebUI directives:
<!DOCTYPE html>
<html lang="en">
<head>
<title>My App</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Hello, {{name}}!</h1>
<for each="item in items">
<my-card>{{item.title}}</my-card>
</for>
<if condition="showFooter">
<footer>Thanks for visiting</footer>
</if>
</body>
</html>
Build Output #
The --out folder will contain:
dist/
โโโ protocol.bin # The WebUI protocol (protobuf binary)
โโโ my-card.css # Component CSS (--css link only)
โโโ nav-bar.css # Component CSS (--css link only)
With --css style, only protocol.bin is written - CSS is embedded directly in the protocol's template fragments.
protocol.bin #
The protocol file contains a serialized WebUIProtocol structure (protobuf binary) with all parsed fragments. This file is consumed by a platform handler at runtime to render HTML with your application state.
The binary format is not human-readable. The equivalent proto schema structure looks like:
// WebUIProtocol
fragments {
key: "index.html"
value: FragmentList {
fragments: [
Raw { value: "<h1>Hello, " },
Signal { value: "name", raw: false },
Raw { value: "!</h1>" },
For { item: "item", collection: "items", fragment_id: "for-1" }
]
}
key: "for-1"
value: FragmentList {
fragments: [
Component { fragment_id: "my-card" },
Signal { value: "item.title", raw: false }
]
}
}
Error Messages #
The CLI provides helpful error messages with suggestions:
โ Failed to read /path/to/app/index.html
caused by: No such file or directory (os error 2)
hint: Try using --entry <file> to specify a different entry file
โ App folder not found: /nonexistent/path
caused by: No such file or directory (os error 2)
hint: Check that the app folder path exists
Plugins #
The --plugin flag loads framework-specific extensions that customize both parsing and rendering behavior. The available plugin identifiers are listed in the Plugins reference. No plugin is enabled by default โ output is plain SSR HTML unless one is selected.
# Load a plugin by name
webui build ./my-app --out ./dist --plugin=<name>
webui serve ./my-app --state ./state.json --plugin=<name>
See Plugins for detailed documentation.
External Component Sources #
The --components flag lets you discover components from npm packages or local directories outside your app folder. This is useful for shared component libraries.
npm Packages #
Pass an npm package name. The package must already be installed in node_modules/.
# Single package
webui build ./my-app --out ./dist --components my-widget
# Scoped package (discovers all sub-packages)
webui build ./my-app --out ./dist --components @reactive-ui
# Specific scoped sub-package
webui build ./my-app --out ./dist --components @reactive-ui/button
npm package requirements:
The package's package.json must have:
| Field | Purpose |
|---|---|
exports["./template-webui.html"] | Path to the component's HTML template |
exports["./styles.css"] | Path to the component's CSS (optional) |
customElements | Path to a Custom Elements Manifest JSON file |
The Custom Elements Manifest provides the component tag name via modules[].declarations[].tagName.
If the package also exposes a root JavaScript entry (exports["."], main,
module, or browser), WebUI treats those components as authored custom
elements. Packages with only template/style exports are HTML-only component
libraries. Their templates render on the server and the framework can activate
them later when needed.
Resolution: The CLI searches for node_modules/ by walking up from the app directory, matching Node.js module resolution behavior. Symlinks (pnpm, npm workspaces) are resolved automatically.
Local Paths #
Pass a filesystem path to discover components the same way the app directory
is scanned. A sibling .ts or .js file marks a component as
authored/interactive. Otherwise the component remains HTML-only.
# Relative path
webui build ./my-app --out ./dist --components ./shared/components
# Absolute path
webui build ./my-app --out ./dist --components /libs/ui-kit
Multiple Sources #
Combine multiple --components flags:
webui build ./my-app --out ./dist \
--components @reactive-ui \
--components ./shared/components \
--components my-widget
Caching #
Discovered npm package components are cached at ~/.webui/cache/components/ to avoid re-traversing on every build. The cache is automatically invalidated when package.json or any template, stylesheet, or manifest used by the selected discovery plugin changes. Local path sources are always re-scanned.
Next Steps #
- Hello World Tutorial - Build your first WebUI app
- Components - Learn about web components
- Template Directives -
<for>,<if>, and{{}} - Platform Handlers - Render protocols with state at runtime