WebUI App Development
Use this skill when building or modifying WebUI applications.
Critical rules (memorize these)
- The template is the UI. All structure lives in
.html. Never document.createElement, innerHTML, insertAdjacentHTML, or appendChild. Show/hide with <if>, repeat with <for>. The only exception is mounting a lazily loaded component.
- CSS owns all styling and animation. Never
el.style.x =, classList.toggle, or adoptedStyleSheets. Bind ?data-active="{{expr}}" and select [data-active] in CSS. Animate with transition, @keyframes, @starting-style - never element.animate() or a JS animation library.
- JavaScript is opt-in. A component needs no
.ts file unless it has an @event, a w-ref for an imperative API, a lifecycle hook, a fetch, or a public method API. WebUIElement, @observable, and @attr are optional - add them only when TypeScript reads/writes the value or it is public API. Otherwise the value belongs in the server state JSON.
- Use the web platform.
<dialog> over a div modal, popover over a JS dropdown, <details> over a JS accordion. Prefer :has(), @container, color-mix(), light-dark(), content-visibility.
- Every template binding must exist in the server state JSON. Missing keys render empty, silently.
- HTML, CSS, TypeScript are separate files. No JSX. No CSS-in-JS. No JS in templates.
- Unwrapped components default to Shadow;
--dom light makes them global Light DOM while authored open wrappers stay Shadow. A sole bare top-level <template> explicitly selects Light and is unwrapped even under the Shadow fallback. Light CSS uses ordinary selectors in its owning CSS tree. Use one sole top-level <template shadowrootmode="open"> when the component needs native <slot> projection, Shadow encapsulation, CSS-heavy frequent restyling, root host events, or Shadow-only selectors such as :host. A <slot> and :host fail only in an effective Light component.
- Components inside
<for> loops do NOT inherit loop variables. Pass data via attributes.
- Text bindings are path lookups; comparisons belong in conditions.
{{count}} and {{user.name}} resolve a dotted state path - nothing else. {{count > 0}} is looked up as a key literally named count > 0 and renders empty. Comparisons go in <if condition="count > 0"> or ?active="{{section == 'guide'}}". Operators: ==, !=, <, >, <=, >=, &&, ||, !. Forbidden everywhere: ternary (? :), function calls, arithmetic (items.length - 1 resolves as a path and silently fails - send a precomputed lastIndex), mixing && with ||, more than 5 logical operators.
w-ref requires braces. w-ref="{inputEl}", never w-ref="inputEl" - non-braced fails the build with invalid-w-ref. Use it only for imperative APIs (focus, scroll, showModal), never to read state.
@attr({ mode: 'boolean' }) for true/false. Present = true, absent = false. Never use string "false".
Quick reference
Most components need only HTML and CSS:
<!-- user-card.html - no .ts file -->
<h2>{{user.name}}</h2>
<if condition="user.isAdmin"><span class="badge">Admin</span></if>
Add a class only when something interactive happens:
import { WebUIElement, attr, observable } from '@microsoft/webui-framework';
export class MyComponent extends WebUIElement {
@attr label = ''; // set by a parent template
@attr({ mode: 'boolean' }) disabled = false;
@observable count = 0; // mutated by increment()
inputEl!: HTMLInputElement; // populated by w-ref="{inputEl}"
increment(): void { this.count += 1; }
onKeydown(e: KeyboardEvent): void { if (e.key === 'Enter') this.submit(); }
}
MyComponent.define('my-component');
webui build ./src --out ./dist --plugin=webui
webui serve ./src --state ./data/state.json --plugin=webui --watch
Full reference
The complete guide covering all template syntax, styling and animation rules, anti-patterns, routing, and a pre-flight checklist:
docs/ai.md
Read that file before generating any WebUI code.
1---2name: webui-dev3description: Build interactive WebUI apps with compiled-template hydration, template syntax, component patterns, and CLI usage.4---56# WebUI App Development78Use this skill when building or modifying WebUI applications.910## Critical rules (memorize these)11121. **The template is the UI.** All structure lives in `.html`. Never `document.createElement`, `innerHTML`, `insertAdjacentHTML`, or `appendChild`. Show/hide with `<if>`, repeat with `<for>`. The only exception is mounting a lazily loaded component.132. **CSS owns all styling and animation.** Never `el.style.x =`, `classList.toggle`, or `adoptedStyleSheets`. Bind `?data-active="{{expr}}"` and select `[data-active]` in CSS. Animate with `transition`, `@keyframes`, `@starting-style` - never `element.animate()` or a JS animation library.143. **JavaScript is opt-in.** A component needs **no** `.ts` file unless it has an `@event`, a `w-ref` for an imperative API, a lifecycle hook, a fetch, or a public method API. `WebUIElement`, `@observable`, and `@attr` are optional - add them only when TypeScript reads/writes the value or it is public API. Otherwise the value belongs in the server state JSON.154. **Use the web platform.** `<dialog>` over a div modal, `popover` over a JS dropdown, `<details>` over a JS accordion. Prefer `:has()`, `@container`, `color-mix()`, `light-dark()`, `content-visibility`.165. **Every template binding must exist in the server state JSON.** Missing keys render empty, silently.176. **HTML, CSS, TypeScript are separate files.** No JSX. No CSS-in-JS. No JS in templates.187. **Unwrapped components default to Shadow; `--dom light` makes them global Light DOM while authored open wrappers stay Shadow.** A sole bare top-level `<template>` explicitly selects Light and is unwrapped even under the Shadow fallback. Light CSS uses ordinary selectors in its owning CSS tree. Use one sole top-level `<template shadowrootmode="open">` when the component needs native `<slot>` projection, Shadow encapsulation, CSS-heavy frequent restyling, root host events, or Shadow-only selectors such as `:host`. A `<slot>` and `:host` fail only in an effective Light component.198. **Components inside `<for>` loops do NOT inherit loop variables.** Pass data via attributes.209. **Text bindings are path lookups; comparisons belong in conditions.** `{{count}}` and `{{user.name}}` resolve a dotted state path - nothing else. `{{count > 0}}` is looked up as a key literally named `count > 0` and renders empty. Comparisons go in `<if condition="count > 0">` or `?active="{{section == 'guide'}}"`. Operators: `==`, `!=`, `<`, `>`, `<=`, `>=`, `&&`, `||`, `!`. **Forbidden everywhere:** ternary (`? :`), function calls, arithmetic (`items.length - 1` resolves as a path and silently fails - send a precomputed `lastIndex`), mixing `&&` with `||`, more than 5 logical operators.2110. **`w-ref` requires braces.** `w-ref="{inputEl}"`, never `w-ref="inputEl"` - non-braced fails the build with `invalid-w-ref`. Use it only for imperative APIs (focus, scroll, `showModal`), never to read state.2211. **`@attr({ mode: 'boolean' })` for true/false.** Present = true, absent = false. Never use string `"false"`.2324## Quick reference2526Most components need only HTML and CSS:2728```html29<!-- user-card.html - no .ts file -->30<h2>{{user.name}}</h2>31<if condition="user.isAdmin"><span class="badge">Admin</span></if>32```3334Add a class only when something interactive happens:3536```typescript37import { WebUIElement, attr, observable } from '@microsoft/webui-framework';3839export class MyComponent extends WebUIElement {40 @attr label = ''; // set by a parent template41 @attr({ mode: 'boolean' }) disabled = false;42 @observable count = 0; // mutated by increment()43 inputEl!: HTMLInputElement; // populated by w-ref="{inputEl}"4445 increment(): void { this.count += 1; }46 onKeydown(e: KeyboardEvent): void { if (e.key === 'Enter') this.submit(); }47}48MyComponent.define('my-component');49```5051```bash52webui build ./src --out ./dist --plugin=webui53webui serve ./src --state ./data/state.json --plugin=webui --watch54```5556## Full reference5758The complete guide covering all template syntax, styling and animation rules, anti-patterns, routing, and a pre-flight checklist:5960**[docs/ai.md](/docs/ai.md)**6162Read that file before generating any WebUI code.