@microsoft/webui 0.0.28 → 0.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ai.md ADDED
@@ -0,0 +1,1624 @@
1
+ ---
2
+ layout: page
3
+ description: Authoritative WebUI framework reference for generating correct application code - template-first authoring rules, template syntax, styling, interactivity, routing, state JSON, and anti-patterns.
4
+ ---
5
+
6
+ # WebUI Framework - AI Reference
7
+
8
+ > **Single-page reference for LLMs.** Everything an AI coding assistant needs to
9
+ > generate correct WebUI code. Read the Rules first - they are the constraints
10
+ > that most often get violated. Deep-dive links are indexed at the bottom.
11
+ >
12
+ > Install the loader skill **once** with
13
+ > `npx skills add microsoft/webui --skill webui-reference` - see
14
+ > [AI Coding Agents](/guide/installation#ai-coding-agents).
15
+ > It reads `node_modules/@microsoft/webui/ai.md`, so upgrading `@microsoft/webui`
16
+ > updates the reference without reinstalling the skill.
17
+ >
18
+ > Links starting with `/guide/` or `/tutorials/` refer to the
19
+ > [documentation site](https://microsoft.github.io/webui/), which tracks the
20
+ > current release rather than your installed version.
21
+
22
+ ## Rules
23
+
24
+ These four rules decide almost every authoring question. When in doubt, re-read
25
+ them before writing code.
26
+
27
+ ### 1. The template is the UI
28
+
29
+ All UI structure lives in `.html` template files. There is no other way to
30
+ create UI.
31
+
32
+ - Never `document.createElement()`, `innerHTML`, `insertAdjacentHTML`,
33
+ `appendChild`, `cloneNode`, or `new DOMParser()` to build UI.
34
+ - Show and hide with `<if>`. Repeat with `<for>`. Swap regions with `<outlet>`.
35
+ - If the markup you need does not exist yet, add it to the template and gate it
36
+ with `<if>` - do not build it from JavaScript.
37
+ - **The single exception** is mounting a lazily loaded component, which has no
38
+ template representation until it is fetched. See
39
+ [Lazy component mounting](#lazy-component-mounting).
40
+
41
+ ### 2. CSS owns all styling and animation
42
+
43
+ All styling lives in `.css` files. All animation is declarative CSS.
44
+
45
+ - Never `el.style.color = ...`, `classList.add/remove/toggle`,
46
+ `setAttribute('style', ...)`, `adoptedStyleSheets`, or injected `<style>` tags.
47
+ - To style from state, bind an attribute in the template and select on it in CSS:
48
+ `?data-active="{{isActive}}"` plus `[data-active] { ... }`.
49
+ - Animate with `transition`, `@keyframes`, `@starting-style`, and view
50
+ transitions. Never `element.animate()`, `requestAnimationFrame` tweening,
51
+ `setInterval` timers, or a JavaScript animation library.
52
+
53
+ ### 3. JavaScript is opt-in, and only for interactivity
54
+
55
+ A component needs **no** `.ts` file at all unless something interactive happens
56
+ in it. Scriptless components still render bindings, `<if>`, and `<for>` on the
57
+ server, and still activate for browser state and soft navigation.
58
+
59
+ - `WebUIElement`, `@observable`, and `@attr` are **optional**. Add them only
60
+ when JavaScript actually reads or writes the value, or when the value is part
61
+ of the component's public API that another module sets.
62
+ - A value that only appears in the template belongs in the server state JSON,
63
+ not in an `@observable`.
64
+ - JavaScript is for event handlers, network calls, focus management, and
65
+ imperative browser APIs. Nothing else.
66
+
67
+ ### 4. Use the web platform
68
+
69
+ Prefer a built-in HTML element or modern CSS feature over a hand-built one.
70
+
71
+ - `<dialog>` over a div-based modal. `popover` over a JS dropdown.
72
+ `<details>` over a JS accordion.
73
+ - `:has()`, `@container`, `@starting-style`, `field-sizing`, `color-mix()`,
74
+ `light-dark()`, `content-visibility`, `inert`, `@layer`.
75
+ - Native form validation, `<input type="...">` variants, `loading="lazy"`.
76
+ - No UI libraries, no CSS frameworks, no polyfills for baseline features.
77
+
78
+ ## What to reach for
79
+
80
+ | You need | Use this | Not this |
81
+ |---|---|---|
82
+ | Show / hide a region | `<if condition="...">` | `el.hidden`, `style.display` |
83
+ | Render a list | `<for each="x in xs">` | `createElement` in a loop |
84
+ | Style from state | `?data-x="{{expr}}"` + CSS attribute selector | `classList.toggle` |
85
+ | Toggle a class-like variant | `?data-variant="{{mode == 'compact'}}"` | `className = ...` |
86
+ | Modal | `<dialog>` + `showModal()` | div overlay + z-index juggling |
87
+ | Dropdown / tooltip / menu | `popover` + `:popover-open` | JS positioning library |
88
+ | Accordion / disclosure | `<details><summary>` | click handler + height math |
89
+ | Tabs | radio inputs or `?data-selected` + CSS | JS show/hide of panels |
90
+ | Enter / exit animation | `@starting-style` + `transition-behavior: allow-discrete` | `element.animate()` |
91
+ | Looping animation | `@keyframes` | `requestAnimationFrame` loop |
92
+ | Page transition | `view-transition-name` + `::view-transition-*` | manual fade with JS |
93
+ | Responsive to container | `@container` | `ResizeObserver` + inline styles |
94
+ | Style a parent from a child | `:has()` | JS class propagation |
95
+ | Theme values | CSS custom properties + `--theme` | JS theme objects |
96
+ | Derived boolean | `<if condition="items.length">` | `@observable hasItems` |
97
+ | Text that never changes on the client | server state JSON | `@observable` |
98
+ | Focus / scroll / measure | `w-ref="{el}"` + a `.ts` file | `querySelector` |
99
+
100
+ ## Mental model
101
+
102
+ **Trusted Types:** WebUI automatically uses its private `webui` policy for compiled
103
+ templates and generated CSS import maps. No setup call or special import order is
104
+ needed. To enforce Trusted Types, use `require-trusted-types-for 'script'; trusted-types webui`
105
+ alongside the normal nonce CSP. Share one framework module across bundles; do not
106
+ pre-create its policy. Raw triple-brace state HTML, FAST strings and arbitrary
107
+ scripts/URLs are not promoted. Client-side router partial navigation is not yet
108
+ supported under enforcement. See
109
+ [Trusted Types](/guide/concepts/hydration#trusted-types).
110
+
111
+ ```
112
+ BUILD TIME SERVER RENDER CLIENT HYDRATION
113
+ --------------- --------------- -----------------
114
+ HTML + CSS + TS -> protocol.bin -> Web Components
115
+ webui build + JSON state hydrate as islands
116
+ -> rendered HTML
117
+ ```
118
+
119
+ WebUI is a **language-agnostic server-side rendering framework**. Templates
120
+ compile to a binary Protocol Buffer at build time. At runtime any backend
121
+ (Rust, Node, Go, C#, Python) supplies JSON state and produces HTML. On the
122
+ client, interactive components hydrate as islands.
123
+
124
+ 1. **Every template binding should exist in the server state JSON.** If the
125
+ template uses `{{title}}`, the server must provide `{ "title": "..." }`.
126
+ Missing text and attribute paths render empty. A missing condition identifier
127
+ is falsy, so `path` is false and `!path` is true. No error is raised.
128
+ 2. **Derived state belongs in the template or the server.** Use expressions like
129
+ `items.length` or `status == 'active'`. Compute complex values server-side.
130
+ 3. **The server is the source of truth for the initial render.** The client
131
+ takes over after hydration for user interactions.
132
+ 4. **Scriptless components are dormant, not dead.** Their bindings render on the
133
+ server and contribute no initial bootstrap state. Compiler-owned hosts can
134
+ still activate for browser-applied state, parent property writes, or soft
135
+ navigation. Events and lifecycle code need a same-named `.ts` or `.js`.
136
+ 5. **Hydration state is client-facing.** It reduces CPU and bytes but is not a
137
+ secrecy boundary. Never put credentials or private tokens in render state.
138
+
139
+ ## Project structure
140
+
141
+ ```
142
+ my-app/
143
+ |- src/
144
+ | |- index.html <- Entry template
145
+ | |- index.ts <- Hydration entry point
146
+ | |- my-component/
147
+ | | |- my-component.html <- Component template
148
+ | | |- my-component.css <- Component styles (scoped)
149
+ | | \- my-component.ts <- Optional: only if interactive
150
+ | \- static-widget/
151
+ | |- static-widget.html <- Scriptless: no .ts needed
152
+ | \- static-widget.css
153
+ |- data/state.json <- Server state for dev
154
+ \- dist/
155
+ ```
156
+
157
+ **Component discovery:**
158
+
159
+ - HTML files with a hyphen in the name are components
160
+ (`my-card.html` -> `<my-card>`).
161
+ - CSS files with the same name are auto-paired.
162
+ - A same-named `.ts` or `.js` file opts the component into authored behavior.
163
+ Most components should not have one.
164
+ - Discovery is recursive through subdirectories.
165
+
166
+ ## Template syntax
167
+
168
+ ### HTML structure
169
+
170
+ Write browser-valid HTML nesting. Native void tags are matched
171
+ case-insensitively, and direct `<col>` / `<tr>` runs receive the browser-implied
172
+ `<colgroup>` / `<tbody>` in compiled client metadata. When an `<if>` or `<for>`
173
+ controls table columns or rows, author the corresponding `<colgroup>` or
174
+ `<tbody>` explicitly so both SSR hydration markers stay in the same browser
175
+ parsing context.
176
+
177
+ ### Text binding
178
+
179
+ ```html
180
+ <span>{{user.name}}</span>
181
+ <p>{{items.length}} items</p>
182
+ ```
183
+
184
+ - `{{expr}}` - HTML-escaped output (safe for user input)
185
+ - `{{{expr}}}` - raw/unescaped output (only for trusted content)
186
+
187
+ Text bindings do path lookups only. They cannot do arithmetic or call functions.
188
+
189
+ ### Conditionals
190
+
191
+ ```html
192
+ <if condition="isLoggedIn">
193
+ <p>Welcome back, {{username}}!</p>
194
+ </if>
195
+
196
+ <if condition="!hasItems">
197
+ <p>No items found.</p>
198
+ </if>
199
+
200
+ <if condition="status == 'active'">
201
+ <span class="badge">Active</span>
202
+ </if>
203
+ ```
204
+
205
+ Operators: `==`, `!=`, `>`, `<`, `>=`, `<=`, `&&`, `||`, `!`
206
+
207
+ **Constraints:** max 5 logical operators per expression; cannot mix `&&` and
208
+ `||` in one expression; no parentheses for grouping; no ternary; **no
209
+ arithmetic**.
210
+
211
+ Each side of a comparison is either a literal (number, quoted string, `true`,
212
+ `false`) or a dotted state path. Anything else is read as a path, so
213
+ `{{currentIndex == items.length - 1}}` looks up a key literally named
214
+ `items.length - 1`, finds nothing, and the condition is silently false. Have the
215
+ server send a precomputed `lastIndex` (or `isLast`) instead. Bare `.length` on an
216
+ array or string does work: `<if condition="items.length > 3">`.
217
+
218
+ ### Loops
219
+
220
+ ```html
221
+ <for each="item in items">
222
+ <div>{{item.name}} - {{item.price}}</div>
223
+ </for>
224
+
225
+ <for each="item in reorderableItems">
226
+ <todo-row key="{{item.id}}" title="{{item.title}}"></todo-row>
227
+ </for>
228
+ ```
229
+
230
+ - The collection must be a JSON array.
231
+ - Nested loops are supported; outer loop variables remain accessible.
232
+ - Recursive trees use a file-local `id` on a defining loop and self-closing
233
+ references with the same item variable. Write `each="item in items"` without
234
+ binding braces:
235
+
236
+ ```html
237
+ <for id="tree-item" each="item in items">
238
+ <div>{{item.name}}</div>
239
+ <for id="tree-item" each="item in item.children" />
240
+ </for>
241
+ ```
242
+
243
+ Define each ID once per file. The definition renders normally, references
244
+ can precede it, and other files may reuse the ID independently. Missing or
245
+ empty child arrays end the recursion; pass finite trees, not cyclic data.
246
+ The recursive `item in item.children` resolves the parent's children first,
247
+ then shadows `item` for the shared body and restores it on return. A second
248
+ body for the same ID is a `duplicate-for-id` build error.
249
+ Use `id`, not `template`; the removed `template` attribute is a build error.
250
+ Native WebUI supports recursive client updates; FAST components reject named
251
+ references with an actionable build error.
252
+ [Recursive loops](guide/concepts/directives/for.md#recursive-loops) covers
253
+ validation and keys.
254
+ - Repeats reconcile by array position by default; item attributes never act as
255
+ keys, and `data-key` is an ordinary application attribute.
256
+ - Add compiler-only `key="{{item.id}}"` to the first concrete child to preserve
257
+ identity across reorder. Leading `<if>` wrappers are transparent; `key`
258
+ directly on `<if>`, `<for>`, or `<outlet>` is invalid.
259
+ - `key="{{item}}"` supports arrays of unique string or finite-number primitives.
260
+ Key paths must be rooted at the loop variable or the build fails with
261
+ `invalid-for-key`. Duplicate or invalid runtime keys warn once and fall back
262
+ to positions.
263
+ - **Components inside loops do NOT inherit loop variables.** Pass data via
264
+ attributes:
265
+
266
+ ```html
267
+ <for each="contact in contacts">
268
+ <contact-card name="{{contact.name}}" email="{{contact.email}}"></contact-card>
269
+ </for>
270
+ ```
271
+
272
+ ### Attributes
273
+
274
+ ```html
275
+ <!-- Dynamic attribute -->
276
+ <a href="{{url}}">{{linkText}}</a>
277
+
278
+ <!-- Boolean attribute (rendered when truthy, omitted when falsy) -->
279
+ <button ?disabled="{{isLoading}}">Submit</button>
280
+ <input type="checkbox" ?checked="{{isSelected}}" />
281
+
282
+ <!-- Boolean attributes accept the same expressions as <if condition="...">.
283
+ Compare against existing state instead of creating mirror observables. -->
284
+ <button ?disabled="{{currentIndex == 0}}">Prev</button>
285
+ <button ?disabled="{{currentIndex == lastIndex}}">Next</button>
286
+ <option ?selected="{{item.id == selectedId}}">{{item.name}}</option>
287
+
288
+ <!-- Mixed static + dynamic -->
289
+ <img src="/img/{{user.avatar}}" alt="{{user.name}}" />
290
+
291
+ <!-- Complex/property binding -->
292
+ <my-widget :config="{{settings}}"></my-widget>
293
+ ```
294
+
295
+ `?attr` is also the styling hook. Bind a `data-*` attribute and select on it in
296
+ CSS rather than touching `classList` from JavaScript.
297
+
298
+ Property bindings use `:` to write directly to DOM properties. For
299
+ client-created trees, initial property bindings are applied before a child's
300
+ `connectedCallback` runs, so children can read parent-provided values during
301
+ setup and still receive later updates through the live binding.
302
+
303
+ ### Events
304
+
305
+ ```html
306
+ <button @click="{handleClick()}">Click me</button>
307
+ <input @keydown="{onKeydown(e)}" />
308
+ <button @click="{selectItem(item.id, 'details', e)}">Select</button>
309
+ <div @mouseenter="{onHover()}" @mouseleave="{onLeave()}">Hover</div>
310
+ ```
311
+
312
+ Handler arguments can be `e`, dotted component or repeat-scope paths, or
313
+ string/number/boolean/null literals. Nested JavaScript expressions are not
314
+ parsed in templates. An `@event` requires a `.ts` file on that component.
315
+
316
+ Each `@event` gets its own listener on the element it is written on - bindings
317
+ are never delegated to a shared root. Non-bubbling events (`@focus`, `@blur`,
318
+ `@mouseenter`, `@load`, `@error`, `@toggle`) therefore work, and an ancestor's
319
+ bubble-phase `stopPropagation()` cannot suppress them.
320
+
321
+ ### DOM references
322
+
323
+ ```html
324
+ <input w-ref="{searchInput}" type="text" />
325
+ ```
326
+
327
+ The braces are **required**. A non-braced `w-ref="searchInput"` fails the build
328
+ with `invalid-w-ref`. Declare the property in the class:
329
+ `searchInput!: HTMLInputElement;`
330
+
331
+ Use `w-ref` only for imperative browser APIs - `focus()`, `scrollIntoView()`,
332
+ `showModal()`, `showPopover()`, measurement. Never use it to read or write
333
+ state that a template binding could express.
334
+
335
+ `w-ref` is scalar. Reusing one ref name inside `<for>` overwrites the same
336
+ property, so the last wired occurrence wins; repeat `key` and position do not
337
+ create an indexed ref collection. For item lookup, use a stable authored `id`
338
+ with `Document.getElementById()` / `ShadowRoot.getElementById()`, or put the ref
339
+ inside an item component.
340
+
341
+ ### The `<template>` tag
342
+
343
+ Unwrapped components default to Shadow. In a `--dom light` build they use
344
+ authored/global Light DOM:
345
+
346
+ A sole bare top-level `<template>` is also an explicit Light wrapper and is
347
+ unwrapped, even when the build fallback is Shadow. Templates with attributes or
348
+ `w-render`/`w-hydrate` are not mode selectors; nested templates remain inert
349
+ template content.
350
+
351
+ ```html
352
+ <!-- my-card.html -->
353
+ <h2>{{title}}</h2>
354
+ <p>{{description}}</p>
355
+ ```
356
+
357
+ In a Light build, use a sole top-level
358
+ `<template shadowrootmode="open">` when the component must remain Shadow for a
359
+ native `<slot>`, native isolation, CSS-heavy frequent restyling, or root host
360
+ events:
361
+
362
+ ```html
363
+ <!-- todo-app.html -->
364
+ <template shadowrootmode="open"
365
+ @toggle-item="{onToggleItem(e)}"
366
+ @delete-item="{onDeleteItem(e)}"
367
+ >
368
+ <for each="item in items">
369
+ <todo-item id="{{item.id}}"></todo-item>
370
+ </for>
371
+ </template>
372
+ ```
373
+
374
+ The wrapper must contain the complete component. `closed`, a dynamic or invalid
375
+ value, placement on another element, multiple declarations, or extra top-level
376
+ content fails the build. `<slot>` is a build error only when the effective mode
377
+ is Light.
378
+
379
+ Root host events catch custom events bubbling up from child components. To
380
+ cross any Shadow boundary between the child and the listener, an event must
381
+ bubble and be `composed`; `this.$emit()` always sets both so Light components
382
+ nested in a Shadow tree work too. A hand-built `new CustomEvent(name)` defaults
383
+ to neither and will never reach the root - pass
384
+ `{ bubbles: true, composed: true }` or bind it on the child element.
385
+
386
+ The binding sits on the host element, so it also catches events targeted at the
387
+ host itself - what host-interactive components (host `tabindex`, presentational
388
+ shadow content) rely on. It does not see non-composed events (`change`,
389
+ `submit`, `select`, media); bind those per element.
390
+
391
+ One root listener also serves an arbitrarily large `<for>`, so this is the way
392
+ to trade per-row listeners for a single handler on a very long list. For rows
393
+ inside the shadow tree `e.target` is the host, so use `e.composedPath()[0]` to
394
+ find the element that was hit.
395
+
396
+ ### Outlet
397
+
398
+ ```html
399
+ <!-- Parent component template -->
400
+ <nav>...</nav>
401
+ <main><outlet /></main>
402
+ ```
403
+
404
+ `<outlet></outlet>` is also valid, but outlets are empty directives and the
405
+ self-closing form is preferred. Use one outlet at each route level, including
406
+ outlets inside nested layout components and directives; extra outlets produce a
407
+ `multiple-outlets` build warning. Child routes have their own outlet level.
408
+
409
+ ### Entry template
410
+
411
+ ```html
412
+ <!DOCTYPE html>
413
+ <html lang="en" dir="{{textdirection}}">
414
+ <head>
415
+ <meta charset="UTF-8">
416
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
417
+ <title>{{title}}</title>
418
+ </head>
419
+ <body>
420
+ <app-shell></app-shell>
421
+ <script type="module" src="/index.js"></script>
422
+ </body>
423
+ </html>
424
+ ```
425
+
426
+ ## Styling
427
+
428
+ Keep ordinary paired component CSS. Light CSS is authored/global in its owning
429
+ Document or ShadowRoot; selectors, keyframes, and cascade layers are not
430
+ rewritten. Shadow components retain native Shadow CSS scoping. No CSS-in-JS or
431
+ styles written from script.
432
+
433
+ ```css
434
+ /* my-component.css */
435
+ my-component {
436
+ display: block;
437
+ padding: 1rem;
438
+ }
439
+
440
+ my-component[disabled] {
441
+ opacity: 0.5;
442
+ pointer-events: none;
443
+ }
444
+
445
+ my-component[variant="primary"] {
446
+ background: var(--colorBrandBackground);
447
+ }
448
+
449
+ .header { font-weight: bold; }
450
+ ```
451
+
452
+ - `:host` styles the component root only in Shadow DOM. In Light DOM, target
453
+ the component tag directly, for example `my-card` or `my-card[disabled]`.
454
+ - Light DOM preserves normal inheritance and global cascade. Parent rules can
455
+ reach Light descendants and matching selectors can affect other Light
456
+ components in the same CSS tree.
457
+ - `:host`, `:host-context`, and `::slotted` fail with
458
+ `unsupported-light-css` in effective Light components; use ordinary
459
+ selectors or author an open Shadow root.
460
+ - `data-wl` and `data-wl-*` are ordinary author attributes; WebUI no longer
461
+ generates or reserves them for Light CSS.
462
+ - Use CSS custom properties for theming. Nested fallbacks like
463
+ `var(--primary, var(--fallback))` are also discovered as tokens.
464
+ - Scoped token overrides do not replace theme defaults for other elements.
465
+ Use bare `:host` rules for shared Shadow component defaults or `:root` for
466
+ document defaults. See [design tokens](/guide/concepts/css-tokens).
467
+ - Malformed CSS fails the build, including unterminated `var()` calls,
468
+ comments, strings, and unmatched delimiters.
469
+
470
+ ### Reactive styling
471
+
472
+ State drives styling through bound attributes, never through script.
473
+
474
+ ```html
475
+ <button ?data-active="{{isActive}}" @click="{toggle()}">{{label}}</button>
476
+ <article ?data-compact="{{density == 'compact'}}">...</article>
477
+ ```
478
+
479
+ ```css
480
+ button[data-active] { background: var(--accent); color: white; }
481
+ button:not([data-active]) { background: transparent; }
482
+ article[data-compact] { --row-height: 1.5rem; }
483
+ ```
484
+
485
+ This works during SSR, needs no synchronization code, and stays correct when the
486
+ server re-renders.
487
+
488
+ ### Modern CSS to prefer
489
+
490
+ | Feature | Use for |
491
+ |---|---|
492
+ | `:has()` | Parent/sibling state without a JS class |
493
+ | `@container` / `cqi` units | Component-level responsiveness |
494
+ | `@starting-style` + `transition-behavior: allow-discrete` | Animating in/out of `display: none`, `popover`, `<dialog>` |
495
+ | `color-mix()` / `light-dark()` / `oklch()` | Derived colors, dual themes without JS |
496
+ | `@layer` | Predictable cascade ordering |
497
+ | `content-visibility: auto` | Long lists without virtualization code |
498
+ | `text-wrap: balance` | Headline typography |
499
+ | `:user-valid` / `:user-invalid` | Form feedback without validation JS |
500
+
501
+ ### Modern HTML to prefer
502
+
503
+ | Element / attribute | Replaces |
504
+ |---|---|
505
+ | `<dialog>` + `showModal()` | Custom modal with overlay and focus trap |
506
+ | `popover` / `popovertarget` | JS dropdown, menu, tooltip, toast |
507
+ | `<details><summary>` | JS accordion or disclosure |
508
+ | `<datalist>` | JS autocomplete |
509
+ | `inert` | Manual `tabindex` juggling |
510
+ | `loading="lazy"` / `decoding="async"` | IntersectionObserver image loaders |
511
+ | `<progress>` / `<meter>` | Div-based bars |
512
+ | Native constraint validation | Hand-rolled validators |
513
+
514
+ ### Progressive enhancement only
515
+
516
+ These are not Baseline across all engines. Use them as an enhancement layer that
517
+ degrades cleanly, never as load-bearing layout or behavior.
518
+
519
+ | Feature | Guard with |
520
+ |---|---|
521
+ | `anchor-name` / `position-area` | `@supports (anchor-name: --x)`, fall back to static placement |
522
+ | Scroll-driven animations | `@supports (animation-timeline: view())` |
523
+ | `field-sizing: content` | A sensible fixed `width` / `rows` default |
524
+ | `text-wrap: pretty` | Harmless when ignored |
525
+ | Customizable `<select>` / `<selectedcontent>` | The native `<select>` rendering |
526
+
527
+ Never polyfill these in JavaScript. If the fallback is unacceptable, use a
528
+ Baseline approach instead.
529
+
530
+ ### Animation
531
+
532
+ Animation is CSS. There is no supported JavaScript animation path.
533
+
534
+ ```css
535
+ /* Enter and exit for a popover or dialog */
536
+ .menu-panel {
537
+ transition: transform 200ms ease, overlay 200ms allow-discrete,
538
+ display 200ms allow-discrete;
539
+ transform: translateX(-100%);
540
+ }
541
+
542
+ .menu-panel:popover-open {
543
+ transform: translateX(0);
544
+ }
545
+
546
+ @starting-style {
547
+ .menu-panel:popover-open {
548
+ transform: translateX(-100%);
549
+ }
550
+ }
551
+
552
+ @media (prefers-reduced-motion: reduce) {
553
+ .menu-panel { transition: none; }
554
+ }
555
+ ```
556
+
557
+ Always honor `prefers-reduced-motion`.
558
+
559
+ ### View transitions
560
+
561
+ The router wraps every client-side navigation in `document.startViewTransition()`
562
+ automatically. **Do not** wrap `Router.navigate()` in your own
563
+ `startViewTransition()` - that would double-transition.
564
+
565
+ Name the transitioning element in **component CSS**:
566
+
567
+ ```css
568
+ /* mp-app.css */
569
+ .route-surface {
570
+ view-transition-name: page-content;
571
+ }
572
+ ```
573
+
574
+ Declare the animations in the **entry template's document-level `<style>`**.
575
+ `::view-transition-*` pseudo-elements live on the document root and cannot be
576
+ reached from inside a shadow root:
577
+
578
+ ```html
579
+ <!-- index.html -->
580
+ <style>
581
+ ::view-transition-old(page-content) {
582
+ animation: 200ms ease-out both fade-out;
583
+ }
584
+ ::view-transition-new(page-content) {
585
+ animation: 300ms ease-in both fade-in;
586
+ }
587
+ @keyframes fade-in { from { opacity: 0; } }
588
+ @keyframes fade-out { to { opacity: 0; } }
589
+ </style>
590
+ ```
591
+
592
+ While the router is active it installs a nonce-bearing
593
+ `@view-transition { navigation: none; }` override, because automatic
594
+ cross-document transitions conflict with intercepted routes that fall back to
595
+ SSR document requests. `Router.destroy()` removes the override. The router
596
+ awaits `updateCallbackDone` (not `.finished`) so rapid navigations supersede
597
+ each other without queuing. Resizing or superseding the animation does not fail
598
+ a committed route; route commit errors still follow normal navigation error handling
599
+ without duplicate unhandled transition rejections.
600
+
601
+ ## Interactivity
602
+
603
+ ### Start scriptless
604
+
605
+ Most components should have only `.html` and `.css`. A scriptless component
606
+ renders bindings, `<if>`, and `<for>` on the server and contributes no bootstrap
607
+ state to the client.
608
+
609
+ ```html
610
+ <!-- user-card.html - no .ts file, nothing to add -->
611
+ <h2>{{user.name}}</h2>
612
+ <p>{{user.title}}</p>
613
+ <if condition="user.isAdmin">
614
+ <span class="badge">Admin</span>
615
+ </if>
616
+ ```
617
+
618
+ ### When to add a `.ts` file
619
+
620
+ Add one only when at least one of these is true:
621
+
622
+ 1. The template has an `@event` handler.
623
+ 2. The template has a `w-ref` you need for an imperative browser API.
624
+ 3. You need `connectedCallback` / `disconnectedCallback` lifecycle work.
625
+ 4. You need to fetch data or call a browser API.
626
+ 5. The component exposes imperative methods or a public property API.
627
+
628
+ If none apply, do not create the file.
629
+
630
+ ### When to add `@observable` or `@attr`
631
+
632
+ Same test, one level down. These decorators exist to connect a value to
633
+ JavaScript - not to make it render.
634
+
635
+ - **`@observable`** - only when TypeScript in this component reads or writes the
636
+ value after hydration. A value that is rendered once and never changed on the
637
+ client belongs in the server state JSON.
638
+ - **`@attr`** - only when the value is part of the component's public API:
639
+ another component, a parent template, or the router sets it as an HTML
640
+ attribute.
641
+
642
+ ```typescript
643
+ // Justified: the click handler mutates it, the template renders it.
644
+ @observable count = 0;
645
+ increment(): void { this.count += 1; }
646
+
647
+ // Justified: a parent template sets label="..." on this element.
648
+ @attr label = '';
649
+
650
+ // NOT justified: nothing in TypeScript touches it - put it in state JSON.
651
+ @observable heading = 'Welcome';
652
+
653
+ // NOT justified: mirrors an expression the template can evaluate.
654
+ @observable hasItems = false; // use <if condition="items.length">
655
+ @observable prevDisabled = true; // use ?disabled="{{currentIndex == 0}}"
656
+ ```
657
+
658
+ ### The component class
659
+
660
+ ```typescript
661
+ import { WebUIElement, attr, observable } from '@microsoft/webui-framework';
662
+
663
+ export class MyComponent extends WebUIElement {
664
+ @attr label = 'Default';
665
+ @attr({ mode: 'boolean' }) disabled = false;
666
+
667
+ @observable count = 0;
668
+ @observable items: Item[] = [];
669
+
670
+ // Populated by w-ref="{inputEl}" in the template
671
+ inputEl!: HTMLInputElement;
672
+
673
+ onSubmit(): void {
674
+ const text = this.inputEl.value.trim();
675
+ if (!text) return;
676
+ this.items = [...this.items, { text, price: 0 }];
677
+ this.inputEl.value = '';
678
+ }
679
+
680
+ onKeydown(e: KeyboardEvent): void {
681
+ if (e.key === 'Enter') this.onSubmit();
682
+ }
683
+
684
+ onItemDelete(e: CustomEvent<{ id: string }>): void {
685
+ this.items = this.items.filter(i => i.id !== e.detail.id);
686
+ }
687
+ }
688
+
689
+ MyComponent.define('my-component');
690
+ ```
691
+
692
+ ### Choose the render and hydration policy
693
+
694
+ Rendering and hydration are separate decisions:
695
+
696
+ - **Rendering** is browser layout/paint work for the server-rendered DOM.
697
+ - **Hydration** is loading JavaScript and attaching bindings/listeners.
698
+
699
+ Agents must use this table instead of treating every form of "lazy" as the same
700
+ behavior:
701
+
702
+ | Root template policy | Rendering before activation | Hydration trigger | Use for |
703
+ |---|---|---|---|
704
+ | No directive | Normal | Eager, when the definition loads | Visible interactive roots and first-use-critical UI |
705
+ | `w-hydrate="lazy"` | Normal | Viewport relevance | Offscreen UI where `content-visibility` containment is unsafe |
706
+ | `w-render="lazy"` + required `w-reserve-block-size` | `content-visibility: auto` skips offscreen layout/paint | Viewport relevance | Repeated or numerous offscreen components; preferred lazy-row policy |
707
+ | `w-hydrate="interaction"` | Normal | Pointer, focus, keyboard, or click intent | One visible app shell/island when startup JS/heap matters more than first-use latency |
708
+ | `w-render="lazy"` + reservation + `w-hydrate="interaction"` | `content-visibility: auto` skips offscreen layout/paint | Interaction intent | One offscreen singleton that should defer both rendering work and JavaScript |
709
+
710
+ #### Agent decision rules
711
+
712
+ 1. **Default to eager hydration for the visible interactive root.** Put lazy
713
+ policies on offscreen descendants when request-to-interactive time matters.
714
+ 2. **For repeated rows/cards, use `w-render="lazy"` with a realistic
715
+ `w-reserve-block-size`.** Do not use interaction hydration for repeated items.
716
+ 3. **Use `w-hydrate="lazy"` only when hydration should wait for the viewport but
717
+ rendering containment could break layout or styling.**
718
+ 4. **Use `w-hydrate="interaction"` only for a singleton shell/island.** It saves
719
+ startup JavaScript and heap but adds first-interaction module-loading latency.
720
+ 5. **For one offscreen singleton that needs both savings, combine
721
+ `w-render="lazy"` with `w-hydrate="interaction"`.**
722
+ 6. **Never combine `w-render="lazy"` with `w-hydrate="lazy"`.**
723
+ `w-render="lazy"` already includes viewport-deferred hydration.
724
+ 7. **Never author `w-render="lazy"` without `w-reserve-block-size`.** The
725
+ reservation should approximate one instance's normal block size.
726
+
727
+ For a component with many initially offscreen SSR instances:
728
+
729
+ ```html
730
+ <template w-render="lazy" w-reserve-block-size="72px">
731
+ <!-- Component content -->
732
+ </template>
733
+ ```
734
+
735
+ For one offscreen SSR singleton whose module graph should remain unloaded until
736
+ use:
737
+
738
+ ```html
739
+ <template
740
+ w-render="lazy"
741
+ w-reserve-block-size="18rem"
742
+ w-hydrate="interaction"
743
+ >
744
+ <!-- Component content -->
745
+ </template>
746
+ ```
747
+
748
+ This keeps rendering containment active while interaction defers JavaScript and
749
+ heap. It is a singleton interaction boundary, not a repeated-item policy.
750
+
751
+ Import the optional coordinator once before component definitions:
752
+
753
+ ```typescript
754
+ import '@microsoft/webui-framework/lazy-hydration.js';
755
+ import './feed-item.js';
756
+ ```
757
+
758
+ On an instance, `w-hydrate="eager"` keeps rendering deferral but hydrates
759
+ immediately; `w-render="eager"` disables both. Use `hydratedCallback()` for work
760
+ that requires bindings or refs. Missing coordinator or browser support falls
761
+ back to eager hydration. Visibility-deferred hydration does not delay image
762
+ fetching; use native `loading="lazy"` and reconcile an already-complete `w-ref`
763
+ image from `hydratedCallback()` when component state depends on `@load` or
764
+ `@error`.
765
+
766
+ To defer a routed application until interaction, mark its root template:
767
+
768
+ ```html
769
+ <template w-hydrate="interaction">
770
+ <nav>...</nav>
771
+ <outlet />
772
+ </template>
773
+ ```
774
+
775
+ ```typescript
776
+ import {
777
+ installInteractionHydration,
778
+ wakeInteractionHydration,
779
+ } from '@microsoft/webui-framework/interaction-hydration.js';
780
+ import { prepareRoutePreload } from '@microsoft/webui-router/preload.js';
781
+
782
+ let prepared: ReturnType<typeof prepareRoutePreload> | undefined;
783
+ const disposeHydration = installInteractionHydration({
784
+ onError: () => prepared?.destroy(),
785
+ load: async () => {
786
+ const [, { Router }] = await Promise.all([
787
+ import('./component-definitions.js'),
788
+ import('@microsoft/webui-router'),
789
+ ]);
790
+ Router.start({ preload: prepared, loaders });
791
+ },
792
+ });
793
+ try {
794
+ prepared = prepareRoutePreload({
795
+ onIntent: () => wakeInteractionHydration(),
796
+ });
797
+ } catch (error) {
798
+ disposeHydration();
799
+ throw error;
800
+ }
801
+ ```
802
+
803
+ The compiler marks the root. Hover stores one bounded raw route partial;
804
+ pointer/focus/keyboard/click intent imports components and router concurrently;
805
+ navigation adopts the response without refetching or parsing templates early.
806
+ Non-router apps use `installInteractionHydration({ load })`. This policy trades
807
+ first-interaction latency for lower startup JS/heap and cannot preserve
808
+ transient user activation or closed-shadow click targets.
809
+
810
+ | Decorator | Purpose | SSR? | Triggers DOM update? |
811
+ |---|---|---|---|
812
+ | `@attr` | HTML attribute reflection | Yes; an existing SSR host attribute wins | Yes |
813
+ | `@attr({ mode: 'boolean' })` | Boolean attribute (present/absent) | Yes; host presence wins | Yes |
814
+ | `@observable` | Reactive state used by TypeScript | Yes (from JSON state) | Yes |
815
+
816
+ | Method / property | Description |
817
+ |---|---|
818
+ | `this.$emit(name, detail?)` | Dispatch a bubbling CustomEvent |
819
+ | `this.$update()` | Force a reactive update cycle |
820
+ | `this.$flushUpdates()` | Synchronously flush pending updates |
821
+ | `<property>Changed(oldValue, newValue)` | For `@attr` and `@observable`, run once after the component mounts with `oldValue` undefined, then synchronously on each live assignment |
822
+ | `protected hydratedCallback()` | Run synchronously once after the first successful hydration or client mount |
823
+ | `static define(tagName)` | Register as a custom element |
824
+ | `defineComponentAssets(manifest)` | Lazy component asset graphs from stable URLs or bundler importer callbacks, with compiler-owned Shadow Link preloading through `preload(tag)` / `create(tag)` |
825
+
826
+ ### Custom events
827
+
828
+ ```typescript
829
+ // Child
830
+ this.$emit('item-selected', { id: this.id, name: this.name });
831
+ ```
832
+
833
+ ```html
834
+ <!-- Parent template -->
835
+ <child-component @item-selected="{onItemSelected(e)}"></child-component>
836
+ ```
837
+
838
+ ```typescript
839
+ // Parent
840
+ onItemSelected(e: CustomEvent): void {
841
+ this.selectedId = e.detail.id;
842
+ }
843
+ ```
844
+
845
+ ### Hydration entry point
846
+
847
+ ```typescript
848
+ // index.ts
849
+ import './app-shell/app-shell.js';
850
+ import './user-card/user-card.js';
851
+ ```
852
+
853
+ Importing a component module registers it as a custom element, which triggers
854
+ hydration. Nothing else is required.
855
+
856
+ ### The hydration boundary
857
+
858
+ Never write `@observable` values before hydration. During SSR hydration the
859
+ server-rendered DOM is trusted and not re-rendered, so a value set in a field
860
+ initializer, the constructor, or before `super.connectedCallback()` cannot
861
+ reach the DOM. The write is dropped and the runtime logs a `[WebUI] Hydration
862
+ mismatch` warning (development-only; stripped from production via
863
+ `__WEBUI_DEV__`).
864
+
865
+ If the value must appear in the first render, put it in the SSR state JSON.
866
+ Otherwise assign it in `hydratedCallback()`. On buffered SSR and client-created
867
+ mounts, `super.connectedCallback()` hydrates synchronously, but streamed hosts
868
+ and visibility-policy hosts (without an eager instance override) can return
869
+ while still deferred.
870
+ `hydratedCallback()` is the cross-mode signal: it runs synchronously exactly
871
+ once after the first successful hydration or mount, and reconnects or callback
872
+ exceptions do not retry it. Once a host has deferred, later state writes
873
+ are retained and replayed; this exception does not make constructor or
874
+ pre-`super.connectedCallback()` writes safe.
875
+
876
+ `@attr` reflects immediately once connected and hydrated.
877
+ `nameChanged(oldValue, newValue)` runs after this element's refs are wired:
878
+ once with `oldValue` undefined for initial state, then synchronously for each
879
+ live assignment. Template bindings remain batched in a microtask.
880
+ Disconnected changes reconcile on reconnect. A throwing callback is not retried;
881
+ `$flushUpdates()` or reconnect resumes other pending callbacks.
882
+
883
+ Load buffered definitions through a parser-inserted, non-async ES module script
884
+ or a classic `defer` script. Descendants must not structurally mutate a
885
+ containing WebUI component's SSR subtree before it hydrates - insertion,
886
+ removal, or reordering shifts compiled element indices.
887
+
888
+ ### Progressive streaming hydration
889
+
890
+ `<boundary>` is a compile-time checkpoint directive for progressive sessions.
891
+ It is valid in entries and reusable components, including runtime conditions,
892
+ outlets, and selected route content.
893
+
894
+ ```html
895
+ <!-- index.html -->
896
+ <head>
897
+ <script type="module" async src="/index.js"></script>
898
+ </head>
899
+ <body>
900
+ <ntp-page></ntp-page>
901
+ </body>
902
+ ```
903
+
904
+ ```html
905
+ <!-- ntp-page.html -->
906
+ <main>
907
+ <boundary name="search-ready">
908
+ <search-box query="{{query}}"></search-box>
909
+ </boundary>
910
+ <section>{{slowFeed}}</section>
911
+ </main>
912
+ ```
913
+
914
+ - `name` is required, non-empty, static, and unique within its entry or
915
+ component owner. It
916
+ cannot contain a <code v-pre>{{binding}}</code>.
917
+ - Boundaries may appear inside reusable components, true `<if>` paths, and
918
+ selected route content. Authored boundaries may not contain another authored
919
+ boundary directly or transitively.
920
+ - A boundary-bearing subtree reached from a `<for>` body fails with
921
+ `boundary-in-repeat`, including declarations reached through a component,
922
+ condition, route, or outlet. A `<for>` may be wholly inside one boundary, and
923
+ boundaries before or after a `<for>` are valid.
924
+ - A component-owned declaration reached from multiple static callsites in one
925
+ entry traversal requires `key`; it must resolve to a unique live string or
926
+ finite number. Independent entries that each reach it once do not.
927
+ - Never author `<webui-hydrate>`. It is reserved generated runtime output.
928
+ - Explicitly import `@microsoft/webui-framework/streaming.js` in application
929
+ code. For a coordinator-only head script, put that import in a small
930
+ `src/streaming.ts` entry and load its output with `type="module" async`.
931
+ WebUI never creates the entry or injects the import. With any bundler, preserve
932
+ initialization, share framework modules, and use native output metadata for
933
+ the host's existing asset handoff. No streaming JSON file is required.
934
+ See
935
+ [Separate coordinator and application assets](/guide/concepts/hydration#separate-coordinator-and-application-assets).
936
+ - Deferred application scripts can use `fetchpriority="low"` to stay out of
937
+ automatic modulepreload hints. Components remain inert and retain pending
938
+ state until their definitions arrive, so load early-interactive registrations
939
+ when needed rather than postponing every registration.
940
+ - `start(state)`, `resume(instanceId, state, mode)`, and `advance()` return a
941
+ step with bytes, optional runtime descriptor
942
+ `{ instanceId, declarationId, owner, name, key }`, and `done`.
943
+ - Drive the step state exactly: descriptor present means `resume`; no descriptor
944
+ with `done == false` means `advance`; `done == true` means complete.
945
+ - `resume` writes only the pending occurrence through its checkpoint.
946
+ `advance` writes the following parent or shell bytes through the next
947
+ occurrence or terminal. No sibling boundary is needed to split an early
948
+ component child from its parent tail.
949
+ - `update(instanceId, patch)` accepts only a committed updatable occurrence.
950
+ It is valid between that occurrence's `resume` and `advance`, and calls
951
+ `setState()` without inserting markup, rerunning hydration, or rerunning
952
+ `hydratedCallback()`.
953
+ - State resolution across a suspension is lexical locals, resume state, then
954
+ the frozen projected parent state.
955
+ - `render_streaming` borrows one state value and keeps one prepared render
956
+ context for the complete response. Host-driven `stream_response` sessions use
957
+ each `resume` state as an overlay for newly resolved occurrence data.
958
+ `resume_current` skips the overlay when retained state is authoritative.
959
+ `start` and `resume` move freshly decoded owned `Value`s or borrow shared
960
+ values through the same method names.
961
+ - Range records may reference the exact prior range state by sequence
962
+ and carry only a top-level delta. Missing, stale, forward, mismatched, or
963
+ malformed references fail closed.
964
+ - A component-local boundary uses generated parent spans. Its early marked child
965
+ may hydrate before the opaque parent tail in light or shadow DOM.
966
+ - `webui:boundary-hydrated` is emitted only when
967
+ `window.__WEBUI_STREAMING_DEBUG__ === true`; its `detail.kind` is
968
+ `checkpoint`, `span`, `update`, or `terminal`. Every commit also emits an
969
+ unconditional `performance.mark()` (`webui:boundary:<id>`,
970
+ `webui:boundary:<id>:update`, `webui:span:<id>`,
971
+ `webui:streaming:terminal`) that tooling can read retroactively.
972
+ `webui:hydration-complete` fires only after the terminal record and eager
973
+ pending hydration work complete. Visibility-deferred lazy roots do not keep
974
+ this one-shot startup event open.
975
+ - `window.__WEBUI_STREAMING_SLICE_MS__` opts into a time-sliced drain that
976
+ yields between boundaries. Use it only when an intermediary coalesces the
977
+ response into one chunk; it costs total hydration time.
978
+ - A semantic flush hands bytes to the HTTP transport. Server adapters,
979
+ compression, proxies, and CDNs can still buffer them.
980
+
981
+ Malformed directives use stable diagnostics:
982
+ `missing-boundary-name`, `invalid-boundary-name`,
983
+ `duplicate-boundary-name`, `missing-boundary-key`,
984
+ `invalid-boundary-key`, `nested-boundary`, `boundary-in-repeat`,
985
+ `boundary-crosses-scope`, and `authored-webui-hydrate`. Malformed browser
986
+ records fail closed and release discoverable deferred state within fixed
987
+ bounds.
988
+
989
+ ### Lazy component mounting
990
+
991
+ This is the **only** sanctioned place to insert an element from JavaScript,
992
+ because a lazily loaded component has no template representation until fetched.
993
+
994
+ Prefer the template-driven form, which needs no DOM insertion at all:
995
+
996
+ ```typescript
997
+ await Router.ensureLoaded('settings-dialog');
998
+ this.showSettings = true;
999
+ ```
1000
+
1001
+ With the framework loaded, `ensureLoaded()` also waits for bounded Link
1002
+ stylesheet cache warming or a native-link fallback decision. Warmup bytes are
1003
+ never applied; the first mounted instance remains guarded until the browser
1004
+ validates its native link and can seed the shared constructable sheet. Redirect,
1005
+ service-worker, authored `<style>`, or inaccessible-CSSOM cases keep the native
1006
+ link rather than risking different response-base or cascade semantics.
1007
+ If an authoritative native link fails, WebUI reports the error, keeps the link
1008
+ native, releases the temporary guard, and completes hydration so the component
1009
+ remains visible and usable even if it is unstyled.
1010
+
1011
+ ```html
1012
+ <if condition="showSettings">
1013
+ <settings-dialog @close="{onCloseSettings()}"></settings-dialog>
1014
+ </if>
1015
+ ```
1016
+
1017
+ Without `@microsoft/webui-router`, prebuild the asset and mount it into a
1018
+ `w-ref` slot:
1019
+
1020
+ ```bash
1021
+ webui build ./src --out ./dist --plugin=webui \
1022
+ --emit-component-assets settings-dialog \
1023
+ --metafile ./dist/component-assets-meta.json
1024
+ ```
1025
+
1026
+ ```typescript
1027
+ import { defineComponentAssets } from '@microsoft/webui-framework/component-asset.js';
1028
+
1029
+ export const settingsAssets = defineComponentAssets({
1030
+ 'settings-dialog': {
1031
+ asset: '/settings-dialog.webui.js',
1032
+ module: () => import('./settings-dialog/settings-dialog.js'),
1033
+ },
1034
+ });
1035
+
1036
+ async onOpenSettings(): Promise<void> {
1037
+ settingsAssets.preload('settings-dialog');
1038
+ this.panelSlot.replaceChildren(await settingsAssets.create('settings-dialog'));
1039
+ }
1040
+ ```
1041
+
1042
+ The compiler stores final Link stylesheet hrefs in the protocol. Shadow builds
1043
+ publish them as inert head JSON that `preload(tag)` consumes automatically;
1044
+ Light builds emit them as document stylesheets. Authored code therefore keeps
1045
+ only the stable root asset URL and never invents or hardcodes a content-hashed
1046
+ stylesheet filename.
1047
+ Automatic Shadow intent preloading requires HTML rendered through the WebUI
1048
+ handler or `Protocol`, which emits `#webui-component-assets`. Using build
1049
+ artifacts without rendering the protocol preserves the guarded native mount but
1050
+ does not provide early compiler-owned style preloading.
1051
+
1052
+ Assets keep entry-owned templates external, inline dependencies used by one
1053
+ asset root, and split dependencies shared by multiple roots into deduplicated
1054
+ dynamic chunks. Do not copy generated chunk filenames into the manifest; each
1055
+ root asset carries its own dynamic imports. `create(tag)` waits for the template
1056
+ graph and module, then creates the element. Failed asset or authored module work
1057
+ is evicted so a later `preload(tag)` or `create(tag)` retries. The normal entry
1058
+ bundle must load first. Component assets cannot be combined with `<route>`; use
1059
+ the router for routed components.
1060
+
1061
+ ## Routing
1062
+
1063
+ ```html
1064
+ <route path="/" component="app-shell">
1065
+ <route path="" component="home-page" exact />
1066
+ <route path="users" component="user-list" exact />
1067
+ <route path="users/:id" component="user-detail" exact />
1068
+ </route>
1069
+ ```
1070
+
1071
+ - Child paths are relative to the parent (no leading `/`).
1072
+ - Use `exact` on leaf routes; omit it on parents that have `<outlet />`.
1073
+ - Path params: `:id` (required), `:query?` (optional), `*path` (catch-all).
1074
+ - Initial SSR delivers CSS only for the matched route chain; inactive route
1075
+ styles remain deferred until navigation. Matched route CSS targeting the
1076
+ Document is applied before `</head>`. ShadowRoot-targeted Link CSS is preloaded
1077
+ from the head and applied inside its owning root. Static request-reachable
1078
+ Shadow roots are preloaded the same way.
1079
+
1080
+ | Attribute | Example | Description |
1081
+ |---|---|---|
1082
+ | `path` | `"users/:id"` | URL path template (relative to parent) |
1083
+ | `component` | `"user-detail"` | Component tag to mount |
1084
+ | `exact` | (boolean) | Require exact path match |
1085
+ | `query` | `"action,to,subject"` | Allowlist of query params set as attributes (deny-by-default) |
1086
+ | `keep-alive` | (boolean) | Preserve DOM and local state across navigations |
1087
+ | `cache-tags` | `"thread:{threadId},inbox"` | Cache tag templates resolved at render time |
1088
+ | `invalidates` | `"inbox,sent,counts"` | Tags auto-invalidated after mutation actions |
1089
+ | `pending` | `"loading-skeleton"` | Loading UI for slow navigations (>150ms) |
1090
+ | `error` | `"error-display"` | Error boundary on fetch failure |
1091
+
1092
+ All attributes are validated at build time. Referencing a non-existent `pending`
1093
+ or `error` component is a compile error.
1094
+
1095
+ ```typescript
1096
+ Router.start({
1097
+ loaders: {
1098
+ 'home-page': () => import('./pages/home-page.js'),
1099
+ 'user-detail': () => import('./pages/user-detail.js'),
1100
+ },
1101
+ });
1102
+ ```
1103
+
1104
+ Route loaders (`static loader({ params, query, signal })`) and actions
1105
+ (`static action({ formData, params, signal })`, enabled by
1106
+ `Router.start({ actions: true })`) live on the component class. Cache and
1107
+ preload are optional runtime tiers; a default `Router.start()` does not load the
1108
+ cache module.
1109
+
1110
+ Every route intended for partial navigation must register a custom element.
1111
+ Scriptless templates are registered by the compiler-owned host runtime. If a
1112
+ route remains unregistered after template publication and loader resolution, the
1113
+ router navigates the document so the server can render it.
1114
+
1115
+ Full detail: [Routing](/guide/concepts/routing).
1116
+
1117
+ ## State JSON
1118
+
1119
+ ```json
1120
+ {
1121
+ "title": "My App",
1122
+ "user": { "name": "Alice", "role": "admin" },
1123
+ "items": [
1124
+ { "id": "1", "label": "First", "done": false },
1125
+ { "id": "2", "label": "Second", "done": true }
1126
+ ],
1127
+ "isAdmin": true,
1128
+ "showBanner": false
1129
+ }
1130
+ ```
1131
+
1132
+ **Path resolution:** `title`, `user.name`, `items.0.label`, `items.length`
1133
+
1134
+ **Missing paths:** text bindings render empty. In conditions, a missing
1135
+ identifier is falsy, so `path` is false and `!path` is true. No error.
1136
+
1137
+ **Route-scoped state.** Each route handler should return only the keys that
1138
+ route's template binds to. Sending full app state on every route wastes
1139
+ bandwidth and render time.
1140
+
1141
+ ### Reserved `$webui` state
1142
+
1143
+ The top-level `"$webui"` object is reserved for trusted host HTML emitted at
1144
+ document boundaries:
1145
+
1146
+ ```json
1147
+ {
1148
+ "$webui": {
1149
+ "headEnd": "<link rel=\"preload\" as=\"image\" href=\"/hero.avif\">",
1150
+ "bodyStart": "<!-- immediately after <body> -->",
1151
+ "bodyEnd": "<script src=\"/livereload.js\"></script>"
1152
+ }
1153
+ }
1154
+ ```
1155
+
1156
+ All three members are optional strings. `headEnd`, `bodyStart`, and `bodyEnd`
1157
+ are emitted raw immediately before `</head>`, after `<body>`, and before
1158
+ `</body>`, respectively. Missing, empty, `null`, or non-string members are
1159
+ ignored. WebUI strips the reserved object from hydration and partial-navigation
1160
+ state, so client-side templates and code cannot read it.
1161
+
1162
+ **Never put request-derived or otherwise untrusted content in `$webui`.** The
1163
+ values are not escaped and can create an XSS vulnerability. See
1164
+ [Integrations](/guide/integrations/) for host-specific rendering details.
1165
+
1166
+ ### Truthiness
1167
+
1168
+ | Value | Truthy? |
1169
+ |---|---|
1170
+ | `true` | Yes |
1171
+ | `false` | No |
1172
+ | `0` | No |
1173
+ | Non-zero number | Yes |
1174
+ | `""` (empty string) | No |
1175
+ | `"false"` (string!) | **Yes** (non-empty string) |
1176
+ | `[]` (empty array) | No (server) / **Yes** (client) - always use `.length` |
1177
+ | `{}` (empty object) | No (server) / **Yes** (client) - test a real field |
1178
+ | `null` / missing key | No |
1179
+
1180
+ **Never use the string `"false"` for boolean state. Use real booleans.**
1181
+
1182
+ **Never test a bare array or object for truthiness.** The server evaluator treats
1183
+ empty collections as falsy; the compiled client condition is plain JS `!!value`,
1184
+ where `[]` and `{}` are truthy. Writing `<if condition="items">` means SSR and
1185
+ hydration can disagree. Write `<if condition="items.length">` instead - that
1186
+ agrees on both sides.
1187
+
1188
+ ## Anti-patterns
1189
+
1190
+ ### Building UI from JavaScript
1191
+
1192
+ ```typescript
1193
+ // WRONG
1194
+ const row = document.createElement('div');
1195
+ row.textContent = item.name;
1196
+ this.list.appendChild(row);
1197
+
1198
+ // WRONG
1199
+ this.list.innerHTML = items.map(i => `<div>${i.name}</div>`).join('');
1200
+ ```
1201
+
1202
+ ```html
1203
+ <!-- RIGHT -->
1204
+ <for each="item in items">
1205
+ <div>{{item.name}}</div>
1206
+ </for>
1207
+ ```
1208
+
1209
+ ### Styling from JavaScript
1210
+
1211
+ ```typescript
1212
+ // WRONG
1213
+ this.panel.style.background = this.isActive ? 'blue' : 'transparent';
1214
+ this.panel.classList.toggle('active', this.isActive);
1215
+ this.shadowRoot.adoptedStyleSheets = [sheet];
1216
+ ```
1217
+
1218
+ ```html
1219
+ <!-- RIGHT -->
1220
+ <div class="panel" ?data-active="{{isActive}}">...</div>
1221
+ ```
1222
+
1223
+ ```css
1224
+ .panel[data-active] { background: var(--accent); }
1225
+ ```
1226
+
1227
+ ### Animating from JavaScript
1228
+
1229
+ ```typescript
1230
+ // WRONG
1231
+ el.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 200 });
1232
+ // WRONG
1233
+ let o = 0; setInterval(() => { el.style.opacity = String(o += 0.05); }, 16);
1234
+ ```
1235
+
1236
+ ```css
1237
+ /* RIGHT */
1238
+ .toast { transition: opacity 200ms ease; opacity: 1; }
1239
+ @starting-style { .toast { opacity: 0; } }
1240
+ ```
1241
+
1242
+ ### Unnecessary framework surface
1243
+
1244
+ ```typescript
1245
+ // WRONG - nothing in TypeScript touches these
1246
+ @observable heading = 'Welcome';
1247
+ @observable subtitle = 'Get started below';
1248
+ ```
1249
+
1250
+ ```json
1251
+ // RIGHT - server state JSON
1252
+ { "heading": "Welcome", "subtitle": "Get started below" }
1253
+ ```
1254
+
1255
+ ### Shadow observables that mirror an expression
1256
+
1257
+ ```typescript
1258
+ // WRONG
1259
+ @observable items: Item[] = [];
1260
+ @observable hasItems = false;
1261
+ onItemsChanged(): void { this.hasItems = this.items.length > 0; }
1262
+ ```
1263
+
1264
+ ```html
1265
+ <!-- RIGHT -->
1266
+ <if condition="items.length">...</if>
1267
+ <button ?disabled="{{currentIndex == 0}}">Prev</button>
1268
+ <option ?selected="{{app.slug == currentApp.slug}}">{{app.name}}</option>
1269
+ ```
1270
+
1271
+ Loop variables compose with outer component state in the same expression, so
1272
+ per-item flags like `isCurrent` in the SSR JSON are almost never needed.
1273
+
1274
+ Text bindings do path lookups only. If you need `{{currentIndex + 1}}` for a
1275
+ 1-based display, that is a legitimate `@observable` or a precomputed state key.
1276
+
1277
+ ### Reading DOM instead of state
1278
+
1279
+ ```typescript
1280
+ // WRONG
1281
+ const value = this.shadowRoot.querySelector('.count').textContent;
1282
+ ```
1283
+
1284
+ Use `@observable` for state TypeScript changes, template bindings for output,
1285
+ and `w-ref` only for imperative APIs.
1286
+
1287
+ ### Hand-built platform primitives
1288
+
1289
+ ```html
1290
+ <!-- WRONG -->
1291
+ <div class="modal-backdrop" @click="{close()}">
1292
+ <div class="modal" role="dialog">...</div>
1293
+ </div>
1294
+
1295
+ <!-- RIGHT -->
1296
+ <dialog w-ref="{dialogEl}" @close="{onClose()}">...</dialog>
1297
+ ```
1298
+
1299
+ `showModal()` gives focus trapping, `inert` backdrop, Escape handling, and
1300
+ top-layer stacking for free.
1301
+
1302
+ ### Also not supported
1303
+
1304
+ 1. **No ternary in templates.** `{{x ? 'yes' : 'no'}}` does not work.
1305
+ 2. **No function calls in bindings.** `{{formatDate(item.date)}}` does not work.
1306
+ Compute on the server or in an event handler.
1307
+ 3. **No mixed `&&` and `||`** in one condition. Split into nested `<if>` blocks.
1308
+ 4. **No parentheses in conditions.**
1309
+ 5. **No JavaScript in HTML templates.** Templates compile to binary.
1310
+ 6. **No JavaScript in CSS.** Use custom properties for dynamic values.
1311
+ 7. **No computed getters for SSR state.**
1312
+ 8. **Components inside `<for>` do NOT inherit loop variables.**
1313
+ 9. **No `import` or `require` in templates.** Components are discovered by file
1314
+ naming convention.
1315
+ 10. **Non-braced `w-ref`** fails the build. Use `w-ref="{name}"`.
1316
+
1317
+ ## Pre-flight checklist
1318
+
1319
+ Before emitting WebUI code, confirm:
1320
+
1321
+ - [ ] No `createElement`, `innerHTML`, `appendChild`, or `insertAdjacentHTML`,
1322
+ except mounting a lazily loaded component.
1323
+ - [ ] No `.style.x =`, `classList`, `setAttribute('style')`, or
1324
+ `adoptedStyleSheets`.
1325
+ - [ ] No `element.animate()`, `requestAnimationFrame` tweening, or animation
1326
+ library. Animation is in `.css`.
1327
+ - [ ] Every `.ts` file exists because of an event, `w-ref`, lifecycle hook,
1328
+ fetch, or public API - not by default.
1329
+ - [ ] Every `@observable` / `@attr` is read or written by TypeScript, or is
1330
+ public API. Otherwise it moved to the state JSON.
1331
+ - [ ] No observable mirrors an expression the template can evaluate.
1332
+ - [ ] Every `{{binding}}`, `<if>`, and `<for>` path exists in the state JSON.
1333
+ - [ ] Every `w-ref` uses braces.
1334
+ - [ ] A built-in element was considered before a hand-built one
1335
+ (`<dialog>`, `popover`, `<details>`).
1336
+ - [ ] `::view-transition-*` rules are in the entry template, not component CSS.
1337
+ - [ ] `prefers-reduced-motion` is honored wherever motion is used.
1338
+ - [ ] No conditions mix `&&` with `||`, use parentheses, or use a ternary.
1339
+ - [ ] Every native `<slot>` resolves to Shadow DOM; in a `--dom light` build its
1340
+ component authors a sole top-level `<template shadowrootmode="open">`.
1341
+ - [ ] A sole bare top-level `<template>` is an explicit Light wrapper and is
1342
+ unwrapped even when the build fallback is Shadow.
1343
+
1344
+ ## Build and run
1345
+
1346
+ For a desktop app, start with zero Rust:
1347
+
1348
+ ```bash
1349
+ npm install @microsoft/webui @microsoft/webui-desktop
1350
+ webui desktop run ./src
1351
+ ```
1352
+
1353
+ Use matching package versions and keep optional dependencies enabled to install
1354
+ the native desktop sidecar. Installing only `@microsoft/webui` does not include
1355
+ desktop support. A standalone Rust CLI can use a matching `webui-desktop`
1356
+ sidecar on `PATH`; see the [CLI reference](./guide/cli/#webui-desktop).
1357
+
1358
+ Restart desktop `run` after source changes; desktop `--watch` is not supported.
1359
+
1360
+ Write a host crate only when you need dynamic route state or IPC. To generate a
1361
+ working progressive scaffold, use `webui desktop init ./my-app`; it creates the
1362
+ entry template, package metadata, `webui-desktop.json`, and a packaged-vs-source
1363
+ Rust runner that shares the same app ID and window title. Existing
1364
+ files are protected unless `--force` is passed. Rust apps depend on
1365
+ `microsoft-webui-desktop` with `native` enabled and forward an opt-in `source`
1366
+ feature to `microsoft-webui-desktop/source`. There is no separate runner or
1367
+ compiler dependency. `webui desktop package` builds
1368
+ optimized runners without default features automatically. Select optional app
1369
+ capabilities with `--runner-features`; use `--debug` only for a debug
1370
+ package. Package targets are `macos-app`, `windows-portable`, and
1371
+ `linux-portable`; `all` writes all three layouts without cross-compiling the
1372
+ runner. App-root packaging accepts `--source`, `--state`, `--assets`,
1373
+ `--build-script`, identity, and window flags without `webuiDesktop` in
1374
+ `package.json`. Put persistent package settings in app-root
1375
+ `webui-desktop.json` instead; explicit flags override them and legacy
1376
+ `webuiDesktop` is accepted only when the new file is absent. See the
1377
+ [CLI reference](./guide/cli/#webui-desktop).
1378
+ Installer generation and signing are not supported. The
1379
+ runtime-only build continues to support all `DesktopRuntime::from_bundle*`
1380
+ entry points. See the [desktop SDK guide](./guide/concepts/desktop.md) for
1381
+ source/bundle construction, window customization, and scoped event subscriptions.
1382
+ Use `frame.window_handle().set_title(...)` or `set_background(Rgba { ... })`
1383
+ for live presentation changes from Rust; these do not change the stable app ID
1384
+ or packaged defaults. The background applies to the native surface, current
1385
+ document root, and later full-page renders. Width, height, titlebar style,
1386
+ background, and the remaining initial window options can all be set through
1387
+ `DesktopAppBuilder::window(WindowOptions { ... })` before `build()`. Live size
1388
+ changes use `set_size(width, height)`; titlebar style remains startup-only.
1389
+ On macOS, bundle `shell.icon_path` must stay inside the bundle without `..` or
1390
+ symlink escapes; source hosts can set an absolute path for Dock artwork.
1391
+ `titlebar.height` sizes the application band; Windows caption buttons default
1392
+ to 32 DIPs independently. Pass `--caption-button-size tall` for
1393
+ 48-DIP buttons without changing the application band height.
1394
+ For custom headers, mark the drag surface `webui-drag` and interactive children
1395
+ `webui-no-drag`; a double-click on the drag surface toggles maximize/restore.
1396
+
1397
+ Pass the client bundler's `--projection-manifest <PATH>` to desktop `run`/`build`,
1398
+ or use `--projection-manifest` for app-root packaging. Rust source
1399
+ hosts set `BuildOptions::projection_manifests`. Desktop and web share navigation
1400
+ state projection; unknown requirements keep the correctness-safe full-state
1401
+ fallback. Supplied manifests must be current and complete.
1402
+
1403
+ For application messages, define one proto3 contract and run
1404
+ `webui desktop ipc generate` to produce Rust/TypeScript APIs. Use generated
1405
+ host calls, renderer request handlers and notification subscriptions, not raw
1406
+ method strings or manual byte encoding. JavaScript 64-bit values are `bigint`,
1407
+ bytes are `Uint8Array`, and maps are `Map<K,V>`. Register Rust handlers and
1408
+ explicit `IpcOptions` before building the frame. Requests return typed
1409
+ promises/futures; notification completion acknowledges admission rather than
1410
+ subscriber completion. Dispose subscriptions and honor cancellation.
1411
+ See [desktop message passing](./guide/concepts/desktop.md#message-passing)
1412
+ for schema generation, the four message flows, and connection lifetime.
1413
+
1414
+ ```bash
1415
+ # Install the native CLIs
1416
+ npm install @microsoft/webui @microsoft/webui-press
1417
+
1418
+ # Dev server with live reload
1419
+ webui serve ./src --state ./data/state.json --plugin=webui --watch
1420
+
1421
+ # Static documentation site
1422
+ webui press build
1423
+
1424
+ # Production build
1425
+ webui build ./src --out ./dist --plugin=webui
1426
+
1427
+ # Inspect the compiled protocol
1428
+ webui inspect ./dist/protocol.bin
1429
+ ```
1430
+
1431
+ `webui press` invokes the separately installed native Press sidecar.
1432
+ It launches Rust directly; Press does not support `--format json`.
1433
+
1434
+ Common flags on both commands: `--entry`, `--css <link|style|module>`,
1435
+ `--dom <shadow|light>` (default `shadow`),
1436
+ `--css-bundle` (merge component stylesheets into shared chunks; not valid with
1437
+ `--css module`), `--components`, `--theme`,
1438
+ `--projection-manifest`, `--emit-component-assets`, `--metafile`,
1439
+ `--format json`.
1440
+
1441
+ On `webui serve` (with or without `--watch`) and `webui press serve`, optionally
1442
+ add `--shutdown-timeout 10` for a ten-second shutdown grace period. Omit it to
1443
+ retain the default wait for an active rebuild with no deadline. Forced shutdown
1444
+ returns nonzero and may leave incomplete outputs; supervised mode reserves
1445
+ stdin. See [bounded shutdown](/guide/cli/#bounded-dev-server-shutdown) for
1446
+ platform limits.
1447
+
1448
+ Unwrapped components default to Shadow. Under `--dom light`, they use
1449
+ authored/global Light DOM while a sole top-level
1450
+ `<template shadowrootmode="open">` remains a Shadow island. A sole bare
1451
+ `<template>` explicitly selects Light and is unwrapped.
1452
+
1453
+ Authoring mistakes fail the build with a structured diagnostic carrying a stable
1454
+ code, source location, snippet, and a `help:` fix. Branch on the `code`, never
1455
+ the message. `--format json` emits one JSON object per error on stdout.
1456
+
1457
+ ```
1458
+ x error: invalid <for> each expression [invalid-for-each]
1459
+ --> index.html:67:5
1460
+ each="person inpeople"
1461
+ help: use the form each="item in collection", e.g. each="todo in todos"
1462
+ ```
1463
+
1464
+ Full flag tables, exit codes, and the error-code list:
1465
+ [CLI Reference](/guide/cli/).
1466
+
1467
+ ### External component discovery
1468
+
1469
+ Native `--components` discovery derives names from `<component-name>.html`,
1470
+ scanning a package's `components/` directory when present or its root otherwise.
1471
+ Template/style exports and CEM names are not interpreted by default discovery;
1472
+ FAST's special metadata-based layouts remain separate.
1473
+ Npm collection spellings `@scope/*` and `@scope/package/*` are accepted.
1474
+ Package subpaths and traversal are not package identifiers; use `./components`
1475
+ or another explicit local path for filesystem discovery.
1476
+ Import browser registrations through the package's module exports separately.
1477
+ Catalog script ownership follows matching `.ts`/`.js` siblings per component;
1478
+ package exports and `.spec.ts` files do not make unrelated scriptless tags
1479
+ require registration imports or projection entries.
1480
+
1481
+ ### WebUI Press
1482
+
1483
+ Use native `webui press build --show=content` or `webui press serve --show=content`
1484
+ to generate Markdown/examples/API panels without Press shell UI. Default mode
1485
+ is `all`; explicit CLI values override config `show` across live reloads.
1486
+ Content mode retains the complete document, themes, SSR, and hydration.
1487
+ Content colors follow the OS, including live preference changes, without
1488
+ reading the full site's saved manual theme. Full mode retains its persisted
1489
+ theme control and applies that choice to native theme tokens and controls.
1490
+ See [WebUI Press](/guide/webui-press) for layout and asset behavior.
1491
+
1492
+ In a WebUI Press template, use paired regions for fallback markup and
1493
+ self-closing regions for empty insertion points:
1494
+
1495
+ ```html
1496
+ <webui-press-region name="home.afterHero" layout="home">
1497
+ <project-summary></project-summary>
1498
+ </webui-press-region>
1499
+ ```
1500
+
1501
+ A matching `regions` entry in `.webui-press/config.json` may replace or clear
1502
+ the HTML, add object state beneath the dotted `regions.*` path, and add a
1503
+ page-scoped `scriptFile`. Omit `html`/`htmlFile` to retain the fallback. Regions
1504
+ resolve before component discovery and compilation; do not manually register
1505
+ their components elsewhere. See [WebUI Press named regions](/guide/webui-press)
1506
+ for the stable built-in region list and full configuration contract.
1507
+
1508
+ ```json
1509
+ {
1510
+ "scripts": {
1511
+ "build:client": "node build-client.mjs",
1512
+ "build:protocol": "webui build ./src --out ./dist --plugin=webui --projection-manifest ./dist/webui-projection.json",
1513
+ "build": "npm run build:client && npm run build:protocol",
1514
+ "dev:server": "webui serve ./src --state ./data/state.json --plugin=webui --projection-manifest ./dist/webui-projection.json --watch"
1515
+ },
1516
+ "dependencies": {
1517
+ "@microsoft/webui": "latest",
1518
+ "@microsoft/webui-framework": "latest"
1519
+ }
1520
+ }
1521
+ ```
1522
+
1523
+ Add `@microsoft/webui-router` for client-side navigation. Passing
1524
+ `--projection-manifest` narrows hydration state to exactly the `@observable` and
1525
+ `@attr` fields your bundle uses; omitting it keeps full state.
1526
+
1527
+ ## Server integration
1528
+
1529
+ Any backend loads `protocol.bin` once and renders with JSON state per request.
1530
+
1531
+ ```rust
1532
+ let protocol = Protocol::from_protobuf(&fs::read("dist/protocol.bin")?)?;
1533
+ let handler = WebUIHandler::new();
1534
+ handler.render(&protocol, &state, &options, &mut writer)?;
1535
+ ```
1536
+
1537
+ For one Rust endpoint that serves both initial documents and router partials,
1538
+ pass the complete per-response options through `ServeRequest`. Always attach a
1539
+ fresh document CSP nonce before calling the helper:
1540
+
1541
+ ```rust
1542
+ let options = RenderOptions::new("index.html", request_path)
1543
+ .with_nonce(csp_nonce);
1544
+ let request = ServeRequest::new(options, accepts_json, inventory_header);
1545
+ let response = serve_request(&protocol, &handler, state, &request)?;
1546
+ ```
1547
+
1548
+ ```javascript
1549
+ const protocol = new Protocol(result.protocol, { plugin: 'webui' });
1550
+ const html = protocol.render(state, { entry: 'index.html', requestPath: req.url });
1551
+ ```
1552
+
1553
+ For repeated renders of one immutable Node state snapshot, call
1554
+ `protocol.prepareState(state)` once and pass the result to
1555
+ `protocol.renderPrepared(prepared, options)`. Prepare a new snapshot when state
1556
+ changes.
1557
+
1558
+ For Rust progressive hydration, create a `StreamingResponse` over a
1559
+ `FlushWriter`. Keep one bounded worker as the response owner, cap admitted
1560
+ renders before spawning it, and configure the transport's flush timeout.
1561
+
1562
+ ```rust
1563
+ let mut page = handler.stream_response(&protocol, &options, &mut writer)?;
1564
+ let mut step = page.start(&initial_state)?;
1565
+ while !step.done {
1566
+ step = match step.boundary.as_ref() {
1567
+ Some(boundary) => {
1568
+ let state =
1569
+ load_state(&boundary.owner, &boundary.name, boundary.key.as_ref())?;
1570
+ page.resume(boundary.instance_id, &state, BoundaryMode::Final)?
1571
+ }
1572
+ None => page.advance()?,
1573
+ };
1574
+ }
1575
+ ```
1576
+
1577
+ With `webui serve --api-port`, a Node or other HTTP backend can return
1578
+ newline-delimited control records instead:
1579
+
1580
+ ```text
1581
+ {"type":"start","version":2,"state":{"query":""}}
1582
+ {"type":"resume","boundary":{"owner":"ntp-page","name":"search-ready"},"state":{"query":""},"mode":"updatable"}
1583
+ {"type":"update","boundary":{"owner":"ntp-page","name":"search-ready"},"state":{"query":"webui"}}
1584
+ ```
1585
+
1586
+ Honor HTTP write backpressure and cap concurrent streams. The CLI uses a
1587
+ capacity-one command channel and matches each `boundary` target by
1588
+ descriptor `owner`, `name`, and optional `key` (plus optional
1589
+ `declarationId`). It keeps response-local instance IDs, the compiled protocol,
1590
+ and browser-facing bytes in Rust. A resume control commits boundary-only bytes;
1591
+ the CLI then calls `advance` internally for the following parent bytes. The
1592
+ control stream needs no advance record. After the backend sends the resume for
1593
+ the final descriptor and closes its body, the CLI's final `advance` completes
1594
+ the response. Returning JSON keeps the buffered state path.
1595
+
1596
+ If the backend refuses a stream request (non-success status such as a `503`
1597
+ from its own concurrency cap), no response bytes were sent, so `webui serve` logs
1598
+ one warning and renders the page from fallback state rather than replacing the
1599
+ app with the upstream error body. A failure *after* the stream is live still
1600
+ fails the response, because bytes already flushed cannot be rewound.
1601
+
1602
+ Equivalent APIs exist for WebAssembly, Python (native `microsoft-webui`
1603
+ package), Go (cgo), and C#. For `Router.ensureLoaded()`, expose
1604
+ `GET /_webui/templates?t=tag1,tag2` backed by
1605
+ `render_component_templates(&tags, &inv)`.
1606
+
1607
+ Full detail: [Integrations](/guide/integrations/).
1608
+
1609
+ ## Where to read more
1610
+
1611
+ | Topic | Page |
1612
+ |---|---|
1613
+ | Directives (`if`, `for`, attributes, `route`) | [/guide/concepts/directives/](/guide/concepts/directives/) |
1614
+ | Component authoring and interactivity | [/guide/concepts/interactivity](/guide/concepts/interactivity) |
1615
+ | Lazy/interaction policy syntax and combinations | [/guide/concepts/directives/lazy](/guide/concepts/directives/lazy) |
1616
+ | Hydration lifecycle, projection, and streaming | [/guide/concepts/hydration](/guide/concepts/hydration) |
1617
+ | Best practices and React-habit pitfalls | [/guide/concepts/best-practices](/guide/concepts/best-practices) |
1618
+ | Performance strategy and measurement | [/guide/concepts/performance](/guide/concepts/performance) |
1619
+ | Routing, loaders, actions, caching | [/guide/concepts/routing](/guide/concepts/routing) |
1620
+ | Design tokens and theming | [/guide/concepts/css-tokens](/guide/concepts/css-tokens) |
1621
+ | CLI flags, diagnostics, exit codes | [/guide/cli/](/guide/cli/) |
1622
+ | WebUI Press named regions | [/guide/webui-press](/guide/webui-press) |
1623
+ | Rust, Node, Python, WASM, FFI, Electron | [/guide/integrations/](/guide/integrations/) |
1624
+ | Build a first app | [/tutorials/hello-world/](/tutorials/hello-world/) |