webui-dev
Build interactive WebUI apps with compiled-template hydration, template syntax, component patterns, and CLI usage.
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. Neverdocument.createElement,innerHTML,insertAdjacentHTML, orappendChild. 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, oradoptedStyleSheets. Bind?data-active="{{expr}}"and select[data-active]in CSS. Animate withtransition,@keyframes,@starting-style- neverelement.animate()or a JS animation library. - JavaScript is opt-in. A component needs no
.tsfile unless it has an@event, aw-reffor an imperative API, a lifecycle hook, a fetch, or a public method API.WebUIElement,@observable, and@attrare 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,popoverover 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 lightmakes 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:hostfail 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 namedcount > 0and renders empty. Comparisons go in<if condition="count > 0">or?active="{{section == 'guide'}}". Operators:==,!=,<,>,<=,>=,&&,||,!. Forbidden everywhere: ternary (? :), function calls, arithmetic (items.length - 1resolves as a path and silently fails - send a precomputedlastIndex), mixing&&with||, more than 5 logical operators. w-refrequires braces.w-ref="{inputEl}", neverw-ref="inputEl"- non-braced fails the build withinvalid-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:
Read that file before generating any WebUI code.
Loading supporting files
microsoft/webui · MIT · Revision d0750d41d34f
Be the first to comment
Share what worked or leave a question for the creator.