WebUI Framework is a high-performance server-side rendering framework that operates without JavaScript runtimes. It separates static and dynamic content at build time, creating an efficient protocol that enables fast server-side rendering in any language (Rust, Go, C#, PHP, Ruby, etc.). On the client, Web Components hydrate as interactive islands — only components that need interactivity ship JavaScript.
The framework consists of four primary modules:
- Protocol: Defines the structural representation of UI components
- Parser: Processes HTML/CSS templates into protocol structures
- Expression: Evaluation*: Handles conditional logic evaluation
- Handler: Renders protocol with state data into final HTML output
- No recursion (all algorithms must be iterative)
- No regular expressions
- Minimal runtime computation
- Protocol Buffers for compact binary serialization
- Buffer consolidation for reduced allocations
- Strict context isolation during processing
- Proactive error handling with actionable messages
The protocol defines the serializable structure representing UI templates. At runtime, the protocol uses protobuf for efficient binary serialization. Types are generated directly from proto/webui.proto using prost — there is no separate domain type layer.
/// The root protocol structure representing a complete webpage configuration.
/// Generated from protobuf `message WebUIProtocol`.
pub struct WebUIProtocol {
/// Map of fragment identifiers to their associated fragment lists.
pub fragments: HashMap<String, FragmentList>,
/// Sorted, deduplicated CSS custom property names used across all
/// components and entry page styles (without `--` prefix).
pub tokens: Vec<String>,
/// Per-component data keyed by tag name (client template + CSS).
pub components: HashMap<String, ComponentData>,
}
/// Per-component metadata populated by the active parser plugin at build time.
/// Framework-neutral: each plugin populates the fields it needs.
/// Generated from protobuf `message ComponentData`.
pub struct ComponentData {
/// Client-side template string for hydration.
/// Populated by the active parser plugin (e.g., f-template HTML for FAST,
/// compiled template JS for WebUI Framework).
pub template: String,
/// Component CSS content for the Module strategy.
pub css: String,
/// External stylesheet href for the Link CSS strategy (e.g. "/my-card.css").
/// Empty for Style/Module strategies and for components without CSS.
/// The handler emits a `<link>` tag in `<head>` only when this is non-empty.
pub css_href: String,
}
/// A list of fragments (needed because protobuf maps cannot have repeated values directly).
pub struct FragmentList {
pub fragments: Vec<WebUIFragment>,
}
/// A mapping of unique fragment identifiers to their corresponding fragment lists.
pub type WebUIFragmentRecords = HashMap<String, FragmentList>;
/// A single fragment — one of several types.
/// Generated from protobuf `message WebUIFragment` with a `oneof fragment` field.
pub struct WebUIFragment {
pub fragment: Option<web_ui_fragment::Fragment>,
}
/// The fragment oneof variants.
pub enum Fragment {
Raw(WebUIFragmentRaw),
Component(WebUIFragmentComponent),
ForLoop(WebUIFragmentFor),
Signal(WebUIFragmentSignal),
IfCond(WebUIFragmentIf),
Attribute(WebUIFragmentAttribute),
Plugin(WebUIFragmentPlugin),
Route(WebUIFragmentRoute),
Outlet(WebUIFragmentOutlet),
}pub struct WebUIFragmentRaw {
/// The content to render.
pub value: String,
}pub struct WebUIFragmentComponent {
/// The identifier for the associated fragment record.
pub fragment_id: String,
}pub struct WebUIFragmentFor {
/// The name representing a singular item (e.g., "person").
pub item: String,
/// The collection name (e.g., "people").
pub collection: String,
/// The identifier for the fragment to render for each item.
pub fragment_id: String,
}pub struct WebUIFragmentSignal {
/// The value or identifier of the signal.
pub value: String,
/// Determines if the value should be rendered as raw content.
pub raw: bool,
}pub struct WebUIFragmentIf {
/// The condition expression to evaluate.
pub condition: Option<ConditionExpr>,
/// The identifier for the fragment record to render if true.
pub fragment_id: String,
}Attribute fragments represent dynamic HTML attributes with various binding types:
pub struct WebUIFragmentAttribute {
/// The attribute name (may include `:` prefix for complex attributes).
pub name: String,
/// For simple dynamic attributes, the signal name.
pub value: String,
/// For mixed (template) attributes, the sub-stream fragment ID.
pub template: String,
/// True for `:`-prefixed complex attributes.
pub complex: bool,
/// True for the first dynamic attribute on a component element.
pub attr_start: bool,
/// True for skipped attributes (class, style, role, data-*, aria-*).
pub attr_skip: bool,
/// True for static attribute values on components.
pub raw_value: bool,
/// For `?`-prefixed boolean attributes, the condition tree.
pub condition_tree: Option<ConditionExpr>,
}Plugin fragments carry opaque data from parser plugins to handler plugins. WebUI does not interpret this data — each parser/handler plugin pair defines its own binary contract.
pub struct WebUIFragmentPlugin {
/// Opaque plugin-specific binary data.
pub data: Vec<u8>,
}Route fragments define declarative URL-based routes linking path templates to fragment bodies.
The parser emits these from <route> elements; the handler uses them for server-side route matching.
pub struct WebUIFragmentRoute {
pub path: String, // URL path template (e.g., "sections/:id")
pub fragment_id: String, // Fragment containing the route body
pub exact: bool, // Require exact path match
pub children: Vec<WebUIFragmentRoute>, // Nested child routes
}There is no global route registry or route tree — routes are inline in the fragment graph
via WebUIFragmentRoute nesting.
Outlet fragments mark where matched child route content renders inside a parent route component.
The parser emits these from <outlet /> elements.
pub struct WebUIFragmentOutlet {}Components use <outlet /> in their templates to declare insertion points:
<template shadowrootmode="open">
<h1>Title</h1>
<main><outlet /></main>
</template>Route declaration: Routes are declared as nested <route> elements in the entry HTML.
Child paths are relative to their parent (no leading /). The HTML nesting IS the route tree:
<route path="/" component="app-shell">
<route path="sections/:sectionId" component="section-page">
<route path="topics/:topicId" component="topic-page">
<route path="lessons/:lessonId" component="lesson-page" exact />
</route>
</route>
</route>Route matching: The handler uses an iterative path template matcher (no regex). Segments are
compared left-to-right: :param binds a value, *splat captures remaining segments, ? marks
optional parameters. Exact matches (most literal segments) take precedence over parameterized ones.
Server-side rendering: When the handler encounters Fragment::Route:
- Pre-scan siblings, pick the best match by specificity.
- Matched route: emit
<webui-route path="..." component="..." active>, render component, recurse into children. - Non-matched routes: emit
<webui-route ... style="display:none">.
For the WebUI framework path, matched route components do not receive route
state as scalar attributes or data-state. Initial SSR state comes from the
rendered DOM plus hydration markers, and client-side navigations apply fresh
state through the partial-response setInitialState(...) path.
When the handler encounters Fragment::Outlet:
- Take children from the currently active route.
- Match children against the request path (relative to route base).
- Emit
<webui-outlet>containing matched child<webui-route>with component, and hidden stubs for siblings.
The handler also emits <meta name="webui-inventory"> in <head> with the initial component
inventory bitmask, so the client router can tell the server which templates it already has
on the first client-side navigation.
Key elements:
<webui-route>— light DOM custom element, structural routing wrapper with no shadow DOM<webui-outlet>— light DOM custom element, marks insertion point for child route content
Client-side navigation:
- On initial load, the router builds the active chain from SSR'd
<webui-route active>elements. - On navigation, fetches a JSON partial (
Accept: application/json) from the server. - The server returns the matched route chain — the client does NOT perform route matching.
- Reconciles old vs new chain — finds first changed level.
- Mounts components at changed levels, creates
<webui-route>stubs at outlet positions. - Parent components and their state are preserved.
Partial response: The server returns { state, templates, inventory, path, chain }:
state: route params from all nesting levels injected into API statetemplates: client template strings the client doesn't already have (filtered by inventory bitmask)inventory: updated hex bitmask of loaded templateschain: matched route chain array — each entry hascomponent,path, optionalparamsandexact
The chain field is produced by render_partial() in the handler, which walks the
fragment graph and matches routes at each nesting level. Host servers call this once per partial
response. Available via FFI as webui_render_partial() for C/.NET/Node hosts.
Partial-template selection: During client navigation, servers derive template names from the normal render fragment graph starting at the persistent entry fragment. The traversal is route-aware but state-agnostic:
- follow
component,if,for, and attribute-template edges conservatively without evaluating request-time state - when a fragment list contains sibling
<route>fragments, follow only the single best match for the current request path using the same specificity rules as SSR - recurse through nested matched route groups so the active route chain is included
- skip unvisited sibling route branches entirely; later navigations will request those templates if needed
- filter the discovered component set against the client's inventory bitmask before returning templates
This intentionally over-ships inactive conditional and loop-driven templates inside the active route chain rather than trying to mirror a transient server-side state snapshot.
Attribute types:
- Simple dynamic:
href="{{url}}"→{ name: "href", value: "url" } - Boolean (
?prefix):?disabled={{isDisabled}}→{ name: "disabled", condition_tree: identifier("isDisabled") }— rendered only if condition is truthy; silently dropped if value is not a pure handlebars expression. - Pass-through / property (
:prefix)::config="{{settings}}"or:value="{{searchQuery}}"→{ name: ":config", value: "settings", complex: true }— reserved for direct pass-through/property bindings, including live form-control values. - Mixed/template:
value="hello {{world}}"→{ name: "value", template: "attr-1" }with sub-streamattr-1: [raw("hello "), signal("world")]
Condition expressions are protobuf messages with a oneof expr field:
/// A condition expression tree (protobuf message with oneof).
pub struct ConditionExpr {
pub expr: Option<condition_expr::Expr>,
}
pub enum Expr {
Predicate(Predicate),
Not(Box<NotCondition>),
Compound(Box<CompoundCondition>),
Identifier(IdentifierCondition),
}
pub struct NotCondition {
pub condition: Option<Box<ConditionExpr>>,
}
pub struct CompoundCondition {
pub left: Option<Box<ConditionExpr>>,
pub op: i32, // LogicalOperator enum value
pub right: Option<Box<ConditionExpr>>,
}
pub struct IdentifierCondition {
pub value: String,
}/// Logical operators for compound conditions (protobuf enum, i32 repr).
pub enum LogicalOperator {
Unspecified = 0,
And = 1,
Or = 2,
}
/// Comparison operators for predicates (protobuf enum, i32 repr).
pub enum ComparisonOperator {
Unspecified = 0,
GreaterThan = 1, // >
LessThan = 2, // <
Equal = 3, // ==
NotEqual = 4, // !=
GreaterThanOrEqual = 5, // >=
LessThanOrEqual = 6, // <=
}pub struct Predicate {
/// The left-hand side value.
pub left: String,
/// The operator used in comparison (ComparisonOperator as i32).
pub operator: i32,
/// The right-hand side value.
pub right: String,
}- Protobuf binary serialization/deserialization as the primary format, using
prostfor direct encode/decode with no conversion layer - Types are generated from
proto/webui.protoat build time viaprost-build - JSON output supported via
webui inspectfor debugging only (using serde derives on generated types) - Support for custom error types and propagation
- Validation of protocol structure during deserialization
- Performance optimizations for large protocol structures
- Support for fragment reference validation
- Attribute names starting with '?' are treated as boolean attributes using the
Attributefragment type with acondition_tree. The attribute is rendered only if the condition evaluates to true.
The find_value_by_dotted_path function provides a high-performance way to query JSON state:
pub fn find_value_by_dotted_path(path: &str, state: &Value) -> Result<Value, StateError>- Dot notation support (e.g., user.profile.name)
- Array indexing support (e.g., users.0.name)
- Special length property support for arrays (e.g., users.length)
- Nullable path handling
- Missing path error reporting
pub fn evaluate(condition: &ConditionExpr, state: &Value) -> Result<bool, ExpressionError>- No recursion: All evaluation must be iterative
- No parentheses: Expression grouping is handled by the ConditionExpr structure
- Logical operators: Support for && (AND) and || (OR) only
- Comparison operators: Support for >, <, ==, !=, >=, <= only
- Negation: Support for ! operator
- No mixed operators: Cannot mix AND and OR in the same expression level
- Operator limit: Maximum of 5 logical operators per expression
- Error handling: Clear, actionable error messages for invalid expressions
pub enum ExpressionError {
MixedOperators,
TooManyOperators(usize),
ValueNotFound(String),
TypeMismatch { expected: String, found: String },
InvalidComparison(String),
// Other error types...
}pub struct WebUIHandler {
plugin: Option<Box<dyn HandlerPlugin>>,
}
/// Options controlling how the handler renders a protocol.
pub struct RenderOptions<'a> {
/// The fragment ID to start rendering from (e.g., `"index.html"`).
pub entry_id: &'a str,
/// The URL path to match routes against (e.g., `"/contacts/42"`).
pub request_path: &'a str,
}
impl<'a> RenderOptions<'a> {
pub fn new(entry_id: &'a str, request_path: &'a str) -> Self;
}
impl WebUIHandler {
pub fn new() -> Self;
pub fn with_plugin(plugin: Box<dyn HandlerPlugin>) -> Self;
pub fn handle(
&mut self,
protocol: &WebUIProtocol,
state: &Value,
options: &RenderOptions<'_>,
writer: &mut dyn ResponseWriter,
) -> Result<()>;
}Route-aware rendering: The handler performs server-side route matching during
rendering. When processing Fragment::Route, the handler matches the route's path
template against options.request_path:
- Matched route: rendered visible (
activeattribute) with component content. - Non-matched routes: rendered hidden (
style="display:none") and empty.
When processing Fragment::Outlet, the handler takes children from the active route,
matches them against the request path relative to the current route base, and emits
<webui-outlet> containing the matched child and hidden stubs for siblings.
This eliminates the need for post-render HTML pruning — the handler produces correct route output in a single pass.
pub trait ResponseWriter {
/// Write content to the output
fn write(&mut self, content: &str) -> Result<()>;
/// Finalize the output
fn end(&mut self) -> Result<()>;
}The handler supports framework-specific hydration plugins. Plugins receive lifecycle callbacks during rendering and write marker formats for their framework, while shared completion work such as rendered-component template emission stays in handler core.
pub trait HandlerPlugin {
fn push_scope(&mut self);
fn pop_scope(&mut self);
fn on_binding_start(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()>;
fn on_binding_end(&mut self, name: &str, writer: &mut dyn ResponseWriter) -> Result<()>;
fn on_repeat_item_start(&mut self, index: usize, writer: &mut dyn ResponseWriter) -> Result<()>;
fn on_repeat_item_end(&mut self, index: usize, writer: &mut dyn ResponseWriter) -> Result<()>;
fn on_element_data(&mut self, data: &[u8], writer: &mut dyn ResponseWriter) -> Result<()>;
fn write_route_component_state(
&self,
state: &serde_json::Value,
writer: &mut dyn ResponseWriter,
) -> Result<()>;
}Hook invocation points:
- Signal:
on_binding_startbefore,on_binding_endafter (same scope) - For loop:
on_binding_start/endaround entire loop;on_repeat_item_start/end+push_scope/pop_scopeper item - If condition:
on_binding_start/endaround condition;push_scope/pop_scopeif condition is true - Component:
push_scope/pop_scopearound component body - Plugin fragment:
on_element_datawith parser-produced hydration bytes from protocol - Matched route component:
write_route_component_statebefore the opening tag closes
Built-in plugin: FastHydrationPlugin
Injects FAST-HTML hydration comment markers for client-side re-hydration:
- Binding:
<!--fe-b$$start$$INDEX$$NAME$$fe-b-->/<!--fe-b$$end$$INDEX$$NAME$$fe-b--> - Repeat:
<!--fe-repeat$$start$$INDEX$$fe-repeat-->/<!--fe-repeat$$end$$INDEX$$fe-repeat--> - Attribute (single):
data-fe-b-INDEX - Attribute (multi):
data-fe-c-INDEX-COUNT
FAST template authoring and runtime usage are documented in the canonical
FAST HTML README.
DESIGN.md only specifies the parser/handler integration contracts.
Built-in plugin: WebUIHydrationPlugin
Injects WebUI Framework SSR hydration markers and attributes:
- Binding:
<!--w-b:start:INDEX:NAME-->/<!--w-b:end:INDEX:NAME--> - Repeat:
<!--w-r:start:INDEX-->/<!--w-r:end:INDEX--> - Attribute (single):
data-w-b-INDEX - Attribute (multi):
data-w-c-INDEX-COUNT - Event:
data-ev="COUNT"once per element with event handlers
Markers are emitted only in active child scopes; the root page scope stays
marker-free. During hydration the runtime walks data-ev elements in DOM order,
consumes COUNT consecutive entries from metadata e[], then installs delegated
shadow-root listeners using runtime data-eh-* attributes on the target elements.
See WebUI Framework Plugin for the protocol details, and
packages/webui-framework/README.md for the
public framework API and authoring model.
Usage:
// FAST plugin
let handler = WebUIHandler::with_plugin(|| Box::new(FastHydrationPlugin::new()));
// WebUI Framework plugin
let handler = WebUIHandler::with_plugin(|| Box::new(WebUIHydrationPlugin::new()));
handler.handle(&protocol, &state, &options, &mut writer)?;- Raw fragments: Write value directly to output
- Signal fragments:
- Resolve value from state using
find_value_by_dotted_path - Escape value if
rawis false, otherwise write as-is
- Resolve value from state using
- Attribute fragments:
- Boolean (with
condition_tree): Evaluate condition; if truthy, render attribute name only. If false, omit entirely. - Simple dynamic (with
value): Resolve signal from state, render asname="resolved_value". - Template (with
template): Rendername=", process referenced sub-stream, render closing". - Pass-through / property (with
complex: true): Same as simple dynamic on the SSR output, but reserved for:prefixed direct pass-through/property bindings.
- Boolean (with
- If fragments:
- Evaluate condition using
evaluate - If true, process referenced fragment
- Track false conditions for template generation
- When the
Iffragments are enclosed in one or moreForfragments it can access the states of thoseForfragments' current item thorugh their corresponding item monikers. It can also access global state. Iffragment conditions can have tokens from different state objects i.e. local states from enclosingForfragment items and/or the global state mixed in the condition expression.
- Evaluate condition using
- For fragments:
- Iterate over collection from state
- Process referenced fragment for each item with current item's state accessible thorugh a moniker and the global state as a fallback.
- Component fragments: Process referenced fragment directly.
Componentfragments enclosed in a For fragment has access to the fields of the current item being looped over and the global state. TheComponentfragment doesn't need to use theForfragment item moniker and can access the fields without the qualification. If theComponentfragment is nested in multipleForfragments only the closest enclosingForfragment item's state is accessible to it. - Plugin fragments: Pass opaque
databytes to the handler plugin'son_element_datahook. Skipped silently when no plugin is configured.
- Global state refers to the global application state that is available to all fragments at all times.
- Local state refers to the state corresponding to the current item being looped over in a
Forfragment. - When nested
Forfragments are present local state of the current item being looped over for any of theForfragment in the hierarchy can be accessed through the corresponding item moniker with an exception forComponentfragments. - For
Componentfragments only the closest enclosingForfragment's current item state is available and can be accessed directly without the item moniker qualification.Componentfragments also have access to the global application state.
- Report missing fragment references
- Handle state resolution failures
- Propagate writer errors
- Validate protocol before processing
- Maximum recursion depth protection
pub struct ComponentRegistry {
components: HashMap<String, Component>,
}
pub struct Component {
pub name: String,
pub html_content: String,
pub css_content: Option<String>,
/// Sorted, deduplicated CSS token names extracted from css_content.
pub css_tokens: Vec<String>,
}pub fn register_component(&mut self, name: &str, html: &str, css: Option<&str>) -> Result<(), ParserError>
pub fn register_directory(&mut self, directory: &Path) -> Result<(), ParserError>
pub fn contains(&self, name: &str) -> bool
pub fn get(&self, name: &str) -> Option<&Component>- Validate component names (must contain hyphen)
- Lazy loading of component content
- Directory scanning with file matching
- Cache optimization for repeated lookups
The webui-discovery crate discovers components from external sources. It has no dependency on webui-parser — it returns plain data structs that callers register into their component registry. This makes it reusable by CLI, FFI, and other host integrations.
enum ComponentSource {
/// npm package: starts with `@` (scoped) or is a bare identifier
NpmPackage(String),
/// Local filesystem path: starts with `.`, `/`, `\`, or drive letter
Path(PathBuf),
}/// Discover components from a single source.
pub fn discover_source(source: &str, search_dir: &Path) -> Result<DiscoveryResult>
/// Collect resolved local paths for file watching.
pub fn collect_watch_paths(sources: &[String], search_dir: &Path) -> Vec<PathBuf>
/// A discovered component (tag name, HTML template, optional CSS).
pub struct DiscoveredComponent {
pub tag_name: String,
pub html_content: String,
pub css_content: Option<String>,
pub source: String,
}- Walk up from the search directory to find
node_modules/(Node.js-style resolution) - For scoped packages (
@scope), enumerate all sub-directories - For each package, read
package.json:exports["./template-webui.html"]→ template HTML pathexports["./styles.css"]→ styles CSS path (optional)customElements→ path to Custom Elements Manifest
- Parse the Custom Elements Manifest for
modules[].declarations[].tagName - Return
DiscoveredComponentstructs (callers handle registration)
Conditional exports are resolved with deterministic priority: default → import → require.
- Path traversal: Export paths are validated — absolute paths and
..components are rejected - Symlink escape: Resolved package paths must remain within
node_modules/afterfs::canonicalize() - File size limits: Manifests and templates are capped at 10 MB to prevent denial-of-service
- Location:
~/.webui/cache/components/ - Cache key: hash of source identifier + resolved path
- Invalidation: hash of
package.jsoncontent (re-discover on change) - Atomic writes: temp file + rename to prevent corruption from concurrent builds
- Corrupt cache files are silently ignored (graceful fallback)
Local paths perform a recursive WalkDir scan for HTML files with hyphenated names, pairing matching CSS files — the same convention used by the parser's ComponentRegistry.
pub struct HtmlParser {
component_registry: ComponentRegistry,
css_parser: CssParser,
condition_parser: ConditionParser,
handlebars_parser: HandlebarsParser,
css_strategy: CssStrategy,
// Other fields...
}/// Strategy for how component CSS is delivered in rendered output.
pub enum CssStrategy {
/// Emit `<link rel="stylesheet" href="./component.css">` tags for
/// components that actually have discovered CSS (default).
Link,
/// Embed CSS content inline in `<style>` tags within the shadow DOM template.
Style,
/// Emit a `<style type="module" specifier="component">` definition once per
/// page and reference it via `shadowrootadoptedstylesheets` on each shadow
/// root `<template>`.
Module,
}- Link (default): Emits
<link>tags referencing external.cssfiles only for components whose discovery/registration data included CSS. Used by the CLI for production builds where CSS files are served separately. - Style: Embeds the full CSS content in
<style>tags inside the shadow DOM template. Used when all files are needed in-memory. - Module: Uses the Declarative CSS Module Scripts proposal. Emits a
<style type="module" specifier="component-name">definition in each component's light DOM (once per component) and addsshadowrootadoptedstylesheets="component-name"to the<template>tag. The browser registers the CSS module globally and shares a singleCSSStyleSheetacross all shadow roots that adopt it. No external CSS files are produced. For partial rendering, CSS module definitions are prepended to client template payloads so the client inserts the definition before the companion template/script runs; WebUI Framework compiled metadata also carries the adopted stylesheet specifier so client-created components can reuse the registered stylesheet.
Set via parser.set_css_strategy(CssStrategy::Style).
pub fn parse(&mut self, fragment_id: &str, html_content: &str) -> Result<(), ParserError>
pub fn into_fragment_records(self) -> WebUIFragmentRecordsThe parser supports a framework-aware plugin system. Plugins classify framework-owned attributes, capture finalized component templates, and emit per-element hydration metadata without requiring the build layer to downcast concrete plugin types.
pub trait ParserPlugin {
fn start_fragment(&mut self, fragment_id: &str) {}
fn register_component_template(
&mut self,
tag_name: &str,
component: &Component,
processed_template: &str,
) -> Result<()>;
fn classify_attribute(&mut self, attr_name: &str) -> AttributeAction;
fn finish_element(&mut self, binding_attribute_count: u32) -> Option<Vec<u8>>;
fn into_artifacts(self: Box<Self>) -> ParserPluginArtifacts;
}Hook invocation points:
- Fragment start:
start_fragmentruns before eachHtmlParser::parse(...)call so plugins can reset fragment-local counters - Attribute loop:
classify_attributedecides whether framework-owned attrs are kept, skipped, or skipped-and-counted as bindings - Element completion:
finish_elementruns with the final binding count after all attrs are processed; returned bytes are emitted as aPluginfragment - Component registration:
register_component_templatereceives the final processed component template HTML - Artifact extraction:
into_artifactsreturns post-parse outputs such as client component templates withoutAnydowncasts
Built-in plugin: FastParserPlugin
- Marks FAST-specific runtime attributes (
@click,f-ref,f-slotted,f-children) as skipped but still counted bindings - Emits
Pluginfragments with u32 LE attribute binding counts - Tracks components and returns
<f-template>artifacts after parsing - Converts syntax to FAST syntax:
<if>→<f-when>,<for>→<f-repeat>,{{expr}}→{expr}in:attrvalues - FAST authoring details live in the canonical FAST HTML README
Built-in plugin: WebUIParserPlugin
- Skips WebUI Framework runtime attributes (
@click,@keydown, etc.) without counting them as attribute bindings - Tracks per-element event count; emits 12-byte
WebUIElementDataPluginfragments encoding[binding_count, event_start, event_count] - Tracks components and compiles templates into raw JS IIFE strings registered in
window.__webui_templates. During SSR the handler wraps them in a single<script>tag; during SPA navigation the router evaluates them directly. - Public framework authoring, decorators, and package entrypoints live in packages/webui-framework/README.md
Usage:
// FAST plugin
let mut parser = HtmlParser::with_plugin(Box::new(FastParserPlugin::new()));
// WebUI Framework plugin
let mut parser = HtmlParser::with_plugin(Box::new(WebUIParserPlugin::new()));
parser.parse("index.html", &html)?;CLI integration:
# FAST plugin
webui build ./templates --out ./dist --plugin=fast
webui serve ./templates --state ./data/state.json --plugin=fast
# WebUI Framework plugin
webui build ./templates --out ./dist --plugin=webui
webui serve ./templates --state ./data/state.json --plugin=webuiwebui serve performs a preflight bind check on its configured HTTP port and
fails before the initial build if that port is already in use, returning an
actionable message so stale dev processes can be stopped explicitly.
- Buffer content until directive or signal encountered
- Consolidate adjacent raw content
- Flush buffer when transitioning to non-raw content
- : Extract item/collection pair and process children into separate fragment. Empty
<for>bodies (no children) are silently skipped. - : Extract and parse condition, process children into separate fragment
- : Injects
body_startandbody_endraw signals around the body content - Components: Check component registry, process as component if found
- Maintain proper tag structure
- Process children recursively (iterative implementation)
- Handle attributes and special elements
- Omit closing tags when the HTML parser produces no end tag (void elements, etc.)
- Handle self-closing tags (
/>syntax) for SVG and other elements
- Buffer Isolation: Isolate directive content from parent context
- Buffer Swapping: Save/restore parent buffer during directive processing
- Final Flush: Ensure all content is captured
pub struct HandlebarsParser;
impl HandlebarsParser {
pub fn parse(&self, text: &str) -> Result<Vec<WebUIFragment>, ParserError>
}- Parse
{{variable}}as escaped signal - Parse
{{{variable}}}as raw (unescaped) signal - No support for nested handlebars expressions
- Iterative implementation (no recursion)
- Proper error handling for malformed expressions
pub struct ConditionParser;
impl ConditionParser {
pub fn parse(&self, input: &str) -> Result<ConditionExpr, ParserError>
}- Identifiers: Simple variable names (e.g.,
isAdmin) - Predicates: Comparisons (e.g.,
age > 18) - Not Expressions: Negations (e.g.,
!isBlocked) - Compound Expressions: Combined with logical operators (e.g.,
isAdmin && isActive)
-- No parentheses support -- String literal support with both single and double quotes -- Proper operator precedence -- No recursion in implementation -- Comprehensive error messages
pub struct CssParser {
parser: Parser,
}
impl CssParser {
pub fn parse(&mut self, css_content: &str) -> Result<WebUIFragmentRecords, ParserError>
pub fn process_css(&mut self, css_content: &str, fragments: &mut WebUIFragmentRecords) -> Result<(), ParserError>
pub fn parse_inline_css(&mut self, style_content: &str) -> Result<String, ParserError>
pub fn extract_tokens(&mut self, css_content: &str) -> Result<HashSet<String>, ParserError>
}- Process CSS variables
- Extract dynamic variables with --webui- prefix
- Convert dynamic variables to signals
- Handle nested variable references
- Process inline and external CSS
CSS Token Hoisting extracts the set of CSS custom properties (tokens) that are used across all components and entry page styles at build time. The sorted, deduplicated list is included in the protocol's tokens field, enabling host runtimes to resolve only the design tokens the application actually needs.
The extract_tokens method uses tree-sitter-css to iteratively walk the CSS AST and extract custom property usages from var() calls, while excluding locally-defined custom properties.
Extracted (hoisted):
var(--colorPrimary)→ token"colorPrimary"var(--a, var(--b, var(--c)))→ tokens"a","b","c"(nested fallbacks)var(--size, 16px)→ token"size"(literal fallbacks ignored)
Excluded (not hoisted):
--bar: 12px— local custom property definitionsvar(--bar)when--baris defined in the same CSS file
The iterative walker visits each call_expression node independently, so nested var() fallbacks (which are separate call_expression nodes in the tree-sitter AST) are naturally handled.
The HtmlParser maintains a token_store: HashSet<String> that accumulates tokens from two sources:
- Component CSS — when a component is first encountered during parsing, its pre-extracted
css_tokens(stored in theComponentstruct at registration time) are merged into the token store. - Inline
<style>tags — when the parser processes astyle_elementnode, it callsextract_tokenson the CSS content and merges the result.
After parsing completes, HtmlParser::take_tokens() returns the sorted, deduplicated token list for inclusion in the protocol.
HTML comments containing handlebars expressions are parsed as signal fragments:
<!--{{tokens}}--> → Signal { value: "tokens", raw: false }
<!--{{{tokens}}}--> → Signal { value: "tokens", raw: true }
<!--{{tokens.light}}--> → Signal { value: "tokens.light", raw: false }
<!-- regular comment --> → Raw (preserved as-is)This mechanism is general-purpose (not limited to tokens) and enables comment-based placeholders for runtime value injection in HTML files. The existing handlebars parser is reused for expression parsing within comment delimiters.
The webui-tokens crate provides serve-time resolution of design token values. While the parser extracts token names into the protocol at build time, the token resolver loads token values from a theme file and generates CSS declarations for injection into state.
A multi-theme JSON file maps theme names to flat token-name → CSS-value objects:
{
"themes": {
"light": { "surface-page": "#ffffff", "text-primary": "#111827" },
"dark": { "surface-page": "#171717", "text-primary": "#fafafa" }
}
}Token names omit the -- prefix (matching the protocol.tokens format). Flat single-theme files (without the "themes" wrapper) are also supported.
load_token_file(path) → TokenFile
↓
resolve_tokens(protocol.tokens, token_file) → ResolvedTokens { css, warnings }
↓
inject_token_css(state, css) → state["tokens"]["light"] = "..."
- Filter: Only tokens in
protocol.tokensare kept. - Dependency closure: Token values referencing other tokens via
var(--x)trigger transitive inclusion. Uses an iterative BFS expansion followed by DFS cycle detection. - CSS generation: Sorted
--name: value;declarations. Output is deterministic. - State injection: Per-theme CSS strings are set on
state.tokens.<theme>, where/*{{{tokens.<theme>}}}*/signals resolve them during rendering.
The CLI --theme flag accepts a file path or an npm package name:
webui serve ./src --theme=@microsoft/webui-examples-theme
webui serve ./src --theme=./my-theme.jsonPackage names are resolved by walking up from search_root looking for node_modules/<pkg>/tokens.json. Scoped packages (@scope/name) and explicit subpaths (@scope/name/custom.json) are supported.
#[derive(Debug, Error)]
pub enum ParserError {
#[error("HTML parsing error: {0}")]
Html(String),
#[error("CSS parsing error: {0}")]
Css(String),
#[error("Directive parsing error: {0}")]
Directive(String),
#[error("Expression parsing error: {0}")]
Parse(String),
#[error("Component error: {0}")]
Component(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}This section specifies only the cross-crate wire contract for --plugin=webui: the metadata emitted by webui-parser, the SSR markers emitted by webui-handler, and the hydration/runtime expectations consumed by @microsoft/webui-framework.
It intentionally does not duplicate package tutorials or framework API docs. Use the canonical sources instead:
- WebUI Framework public API, decorators, and component authoring: packages/webui-framework/README.md
- FAST template authoring and runtime reference: FAST HTML README
Each component's compiled template is registered in window.__webui_templates[tagName] as a marker-free metadata object consumed by the browser runtime:
| Field | Type | Description |
|---|---|---|
h |
string |
Marker-free static HTML for client-created DOM, including baked-in <link> / <style> nodes for link/style CSS strategies |
tx |
[slot, parts][] |
Client text runs inserted at precompiled slots |
a |
CompiledAttrMeta[] |
Attribute binding metadata |
ag |
[elementPath, start, count][] |
Attribute-target groups for a[] |
c |
[ConditionExpr, blockIndex][] |
Conditional blocks |
cl |
SlotPath[] |
Conditional anchor slots aligned to c[] |
r |
[collection, itemVar, blockIndex][] |
Repeat blocks |
rl |
SlotPath[] |
Repeat anchor slots aligned to r[] |
e |
[event, handler, needsEvent][] |
Body events |
el |
NodePath[] |
Event target element paths aligned to e[] |
b |
TemplateBlockMeta[] |
Nested compiled block table referenced by c / r |
sa |
string |
Optional module-mode adopted stylesheet specifier copied from shadowrootadoptedstylesheets |
re |
[event, handler, needsEvent][] |
Root events, attached to the host element |
All arrays are optional — omitted from the output when empty to minimize payload.
ConditionExpr in compiled framework metadata reuses the protocol condition AST in a compact tuple form:
[0, path]— identifier / truthy path lookup[1, left, operator, right]— predicate comparison[2, condition]— logical NOT[3, left, operator, right]— logical compound (AND/OR)
Comparison operators match the protocol enum values:
1=GREATER_THAN2=LESS_THAN3=EQUAL4=NOT_EQUAL5=GREATER_THAN_OR_EQUAL6=LESS_THAN_OR_EQUAL
Logical operators also match the protocol enum values:
1=AND2=OR
a[] uses compact tuple forms to avoid runtime parsing:
[name, 0, path]— simple attribute binding, e.g.href="{{url}}"[name, 1, path]— pass-through/property binding, e.g.:config="{{settings}}"or:value="{{searchQuery}}"[name, 2, ConditionExpr]— boolean attribute binding, e.g.?disabled="{{expr}}"[name, 3, parts]— mixed/template attribute binding, e.g.class="item {{state}}"
The Rust compiler (generate_compiled_template in webui-parser/src/plugin/webui.rs) transforms the HTML template in a single forward pass, then finalizes it into marker-free client HTML plus locator metadata:
| Source syntax | Metadata field(s) | Client h result |
|---|---|---|
{{expr}}, {{{expr}}}, mixed text |
tx[] |
dynamic text run removed |
href="{{url}}" |
a[] + ag[] |
element kept marker-free |
class="item {{state}}" |
a[] + ag[] |
element kept marker-free |
?disabled="{{expr}}" |
a[] + ag[] |
element kept marker-free |
:config="{{settings}}", :value="{{searchQuery}}" |
a[] + ag[] |
element kept marker-free |
<if condition="expr">body</if> |
c[] + cl[] + b[] |
block removed; anchor slot stored |
<for each="v in coll">body</for> |
r[] + rl[] + b[] |
block removed; anchor slot stored |
@event="{handler(e)}" |
e[] + el[] |
element kept marker-free |
@event on <template> wrapper |
re[N] |
(stripped) |
w-ref="name" |
(stays) | (unchanged) |
<outlet /> |
(stays) | <outlet></outlet> |
tx[] stores text runs as [slot, parts], where parts reuse the compact attribute-part encoding (string for static text, [path] for dynamic text). Client-created DOM inserts one runtime Text node per run instead of scanning compiled marker comments.
Attribute bindings are recorded in a[], while ag[] points at the owning element and the contiguous [start, count) range inside a[]. The compiled client HTML never embeds data-w-* markers; those remain SSR-only handler markers.
Nested <if> / <for> blocks are recursively compiled into the shared b[] block table. The client runtime instantiates compiled child blocks directly and evaluates precompiled condition AST tuples — it does not parse raw template syntax or condition strings from repeat or conditional body content.
The private workspace package packages/webui-test-support (@microsoft/webui-test-support) exists to build this metadata shape in JS-side tests without duplicating tuple encodings or fixture infrastructure across webui-framework and webui-router. It centralizes fixture builders such as buildTemplate, registerCompiledTemplate, and the condition AST helpers, and it also provides shared Node-side fixture bundling/server helpers so browser fixture apps and Playwright servers stay aligned with the runtime/compiler contract as that contract evolves.
The current WebUI parser emits a 12-byte Plugin fragment (WebUIElementData) for each element that has attribute bindings or @event handlers:
Bytes 0–3: binding_count (u32 LE) — number of dynamic attribute bindings
Bytes 4–7: event_start_idx (u32 LE) — global index into metadata `e[]`
Bytes 8–11: event_count (u32 LE) — number of @event attrs on this element
The handler decodes this in on_element_data and emits SSR-only markers:
data-w-b-Nfor one bound attribute, ordata-w-c-START-COUNTfor multiplea[]entries on the same elementdata-ev="COUNT"once per element, whereCOUNTis the number of consecutive entries in the metadatae[]array that belong to that element
For compatibility during mixed parser/handler rollouts, the handler also accepts the legacy 4-byte binding-only payload and upgrades it to event_count = 0.
WebUI SSR marker formats are:
| Marker | Format | Notes |
|---|---|---|
| Repeat block start | <!--wr--> |
Opens a <for> loop region |
| Repeat block end | <!--/wr--> |
Closes the <for> loop region |
| Repeat item | <!--wi--> |
Marks each iteration boundary inside a repeat |
| Conditional start | <!--wc--> |
Opens an <if> block |
| Conditional end | <!--/wc--> |
Closes the <if> block |
The WebUI handler plugin emits only these five comment markers. Text bindings, attribute bindings, and event handlers are resolved from compiled metadata path indices at hydration time - no DOM attribute markers are needed. The handler only emits markers in active child scopes; the root page scope remains marker-free. During hydration the framework keeps <!--wr--> and <!--wc--> as runtime anchors and removes <!--/wr-->, <!--/wc-->, and <!--wi--> markers.
@microsoft/webui-framework consumes the metadata object above plus the SSR markers emitted by WebUIHydrationPlugin. This follows an Islands Architecture approach: the server delivers fully-rendered HTML, and only interactive Web Components hydrate on the client — leaving static content untouched.
- SSR hydration uses one DOM walk to discover
<!--wr-->,<!--wi-->, and<!--wc-->comment markers, wire the relevant bindings using compiled metadata path indices, then remove SSR-only markers. - Client-created DOM never reparses template syntax; it clones marker-free
hand resolvestx,ag,cl,rl, andellocators directly. - Events are resolved from compiled
e[]andel[]metadata entries using path indices. The runtime installs one delegated listener per event type on the shadow root. Root events fromre[]attach directly to the host element. - The full package entrypoint supports repeat metadata (
r[]/rl[]). The additive@microsoft/webui-framework/element-no-repeatentrypoint preserves the same publicWebUIElementAPI but must reject compiled templates that contain repeat metadata.
Detailed component examples, decorators, and package entrypoint guidance live in packages/webui-framework/README.md rather than being duplicated in this design spec.
- Unit tests for each module
- Integration tests for complete pipeline
- Performance benchmarks
- Error case coverage
webui/
├── crates/
│ ├── webui/ # Programmatic library API (build, inspect, re-exports)
│ ├── webui-cli/ # CLI build tool (binary: "webui")
│ ├── webui-discovery/ # External component discovery (npm, paths)
│ ├── webui-expressions/ # Expression evaluation engine
│ ├── webui-ffi/ # C-compatible FFI bindings
│ ├── webui-handler/ # Protocol handler implementation
│ ├── webui-node/ # Node.js native addon (napi-rs)
│ ├── webui-parser/ # HTML/CSS/template parser
│ ├── webui-protocol/ # Protocol definition
│ ├── webui-state/ # State management
│ ├── webui-test-utils/ # Testing utilities
│ └── webui-wasm/ # WebAssembly bindings
├── packages/
│ ├── @microsoft/
│ │ ├── webui/ # npm package (CLI + programmatic JS API)
│ │ ├── webui-darwin-arm64/ # Platform binary (macOS ARM64)
│ │ ├── webui-darwin-x64/ # Platform binary (macOS x64)
│ │ ├── webui-linux-x64/ # Platform binary (Linux x64)
│ │ ├── webui-linux-arm64/ # Platform binary (Linux ARM64)
│ │ ├── webui-win32-x64/ # Platform binary (Windows x64)
│ │ └── webui-win32-arm64/ # Platform binary (Windows ARM64)
│ ├── webui-framework/ # WebUI Framework client runtime (@microsoft/webui-framework)
│ ├── webui-router/ # SPA router for WebUI Framework (@microsoft/webui-router)
│ └── webui-test-support/ # Private shared JS test metadata helpers (@microsoft/webui-test-support)
├── examples/ # Example applications (todo-fast, todo-webui, routes, …)
├── docs/ # Documentation (VitePress)
├── tests/ # Integration tests
└── benchmarks/ # Performance benchmarks
webui-cli ──────► webui (library) ◄────── webui-node
│ │
├── webui-parser ├── webui-handler
├── webui-handler ├── webui-protocol
├── webui-protocol └── serde_json
└── webui-discovery
webui-ffi ──────► webui-handler ◄────── webui-wasm
webui-parser webui-parser
webui-protocol webui-protocol
The webui library crate is the primary API surface for programmatic use.
It re-exports WebUIHandler, ResponseWriter, and WebUIProtocol from their
respective crates and provides build(), build_to_disk(), and inspect()
functions with BuildStats (duration, fragment/component/CSS counts, protocol size).
The @microsoft/webui npm package follows the esbuild single-package model:
bin: { "webui": "bin/webui" }— CLI binary via platform-specificoptionalDependenciesmain: "lib/main.js"— Programmatic API that loads the.nodenative addon directly- WASM fallback for render when native addon is unavailable (one-time warning logged)
- Using
vitepressindocs/ - API documentation for all public interfaces
- Technical explanations of algorithms
- Performance considerations
- Error handling guidelines
- Examples for all major features
The FFI crate exposes WebUI to host languages via a C-compatible ABI. The generated
header is at crates/webui-ffi/include/webui_ffi.h.
| Function | Description |
|---|---|
webui_render(html, data_json) |
Parse + render in one call. Returns heap-allocated string (caller frees with webui_free). |
webui_handler_create() |
Create a reusable handler (no plugin). |
webui_handler_create_with_plugin(plugin_id) |
Create a handler with a named plugin (e.g. "fast"). Returns NULL on error. |
webui_handler_render(handler, data, len, json, entry_id, request_path) |
Render a pre-compiled protocol with route matching. request_path controls which route is active. Returns heap-allocated string. |
webui_render_partial(protocol_data, len, state_json, entry_id, request_path, inventory_hex) |
Produce a complete JSON partial response (state, templates, inventory, path, and matched route chain) in a single call. Returns heap-allocated JSON string. |
webui_handler_destroy(handler) |
Destroy a handler. NULL is a safe no-op. |
webui_free(ptr) |
Free a string returned by any render function. NULL is a safe no-op. |
webui_last_error() |
Return per-thread error message. Caller must not free. |
Thread-local error storage following the POSIX dlerror() pattern. After any
function returns NULL, call webui_last_error() for a human-readable diagnostic.
The CLI specification and usage details are maintained in crates/webui-cli/README.md.
Examples and end-to-end walkthroughs are maintained in examples/README.md