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>] [--plugin <NAME>] [--components <SOURCE>]... [--emit-component-assets <TAGS>] [--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 |
--plugin <NAME> | Load a parser plugin | (none) |
--dom <STRATEGY> | DOM strategy: shadow or light | shadow |
--components <SOURCE> | Additional component sources (npm packages or local paths). Repeatable. | (none) |
--emit-component-assets <TAGS> | Comma-separated root component tags to emit as static WebUI component assets in --out | (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, and --servedir support absolute paths, relative paths, ~/..., and file://... URI-style values.
CSS Modes:
| Mode | Behavior |
|---|---|
link | Emits <link> tags referencing external .css files. CSS files are copied to the output folder. |
style | Embeds CSS content directly in <style> tags inside shadow DOM templates. No separate CSS files are written. |
module | Emits <script type="importmap">{"imports":{"component":"data:text/css,..."}}</script> tags that register each component's CSS under a data URI, and adds shadowrootadoptedstylesheets to <template> tags. The browser shares a single CSSStyleSheet across all shadow roots that adopt it. No separate CSS files are written. Based on the Import Maps and CSS Module Scripts proposals. If a component supplies its own <template> wrapper (e.g. to attach @event handlers), WebUI preserves the wrapper attributes and appends shadowrootadoptedstylesheets="component-name" when it is missing. |
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.
Component assets:
Use --emit-component-assets with the WebUI plugin to prebuild CDN-loadable
template assets for components that are not included in initial SSR, such as
route branches or dialogs loaded without @microsoft/webui-router:
webui build ./my-app --out ./dist --plugin=webui \
--emit-component-assets mail-thread,compose-page
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. Assets are emitted to the same output folder as
standard ESM modules, for example mail-thread.webui.js. Each module
default-exports plugin-specific template/style metadata and includes compiled
WebUI condition closures in the same request. FAST plugin builds can emit the
same module shape with <f-template> payloads, but need a FAST-owned runtime
loader rather than the WebUI Framework loader. Asset emission is parallelized
across requested root tags. The module intentionally omits inventory state
because a static CDN asset cannot know the page's current loaded template bitset.
Use --asset-file-name-template "[name]-[hash].[ext]" for long-lived CDN
caching; [hash] is the emitted asset module's SHA-256 content hash truncated
to 8 hex characters. Protocol, CSS, and component asset filenames are validated
as one output set before any files are written, so collisions fail without
leaving partial output.
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(...).
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.
DOM Strategies:
| Strategy | Behavior |
|---|---|
shadow | Components render inside <template shadowrootmode="open">. Style encapsulation via Shadow DOM. Default. |
light | Components render as direct children. No shadow boundary. 26% faster FCP on high-component-count pages. |
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
# 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 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
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>] [--plugin <NAME>] [--components <SOURCE>]... [--api-port <PORT>] [--emit-component-assets <TAGS>] [--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 |
--plugin <NAME> | Load parser + handler plugins (e.g., webui) | (none) |
--dom <STRATEGY> | DOM strategy: shadow or light | shadow |
--components <SOURCE> | Additional component sources (npm packages or local paths). Repeatable. | (none) |
--api-port <PORT> | Proxy route requests to your API server on this port. The dev server forwards navigation requests so your backend can provide real state data. | (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) |
--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, and asset 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
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
# 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. The compiled <tag>.webui.js modules
are served from memory (and rebuilt on change under --watch), so no separate
webui build step or --out directory is needed during development.
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 |
/<tag>.webui.js | In-memory static component assets emitted by --emit-component-assets (served as JS modules) |
/* | Static files from --servedir (when provided) |
/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.
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.
# 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 a package's package.json 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