Appearance
Web Components
Matrix UI is built on the W3C Web Components standard — customElements.define, shadow DOM, slots, attributes. There is no React, no Vue, no Svelte, no JSX runtime. This page explains why, and what that means for the components you write.
Why Web Components
Three reasons govern this choice:
- The browser is a runner. Every tab connects to NATS, instantiates
MatrixRuntime, and mounts a tree of actors. The actor model is the architecture; the framework wraps each actor instance in a custom element so the DOM tree IS the actor tree. Custom elements are the cheapest possible binding. - No build-time framework lock-in. A
MatrixActorHtmlElementsubclass is a TypeScript class that compiles down to standard ES2022. Anyone consuming a Matrix component pays for nothing beyond the standard browser APIs. Components written in this repo can be loaded from any plain HTML page. - Shadow DOM gives us style isolation for free. Each component owns its template and its CSS without leaking. Theming flows through CSS custom properties (the
--mx-*token system declared instatic tokens).
The trade-off: we do without a virtual-DOM diff, JSX, or component composition syntax sugar. We compensate with morphdom (used by the reactive render(state) path) and by composition through the actor tree, not the template tree.
What every component is
A Matrix UI component is a custom element that:
- Extends
MatrixActorHtmlElement(which extendsHTMLElement). - Declares
static accepts/static emits— same contract as any headless actor. - Either supplies its own handler methods (Pattern A) or delegates to a separate
MatrixActorsubclass (Pattern B). See MatrixActorHtmlElement. - Optionally declares a
static template: stringfor shadow DOM, or implementsrender(state)for reactive rendering. - Is registered via
customElements.define('my-tag', MyComponent).
The framework provides the rest:
- The
$joinhandshake with its parent shell. - The auto-pull context cascade (child finds parent shell, gets context, derives mount).
- The mount path computation (DOM nesting → mount path).
- The shadow DOM attachment in the constructor.
- Microtask move detection (so a component that detaches and re-attaches in the same tick keeps its actor).
Naming conventions
Custom-element names must contain a hyphen (a Web Components rule). Inside Matrix:
- Shell-provided primitives:
mx-*(e.g.mx-split,mx-theme,mx-topic-tag). These ship from@open-matrix/core/framework/components/. - Package-specific components:
<package>-*(e.g.director-explorer,chat-conversation,flowpad-canvas). The package owns the prefix. - Page-shell composition root:
<matrix-dsl-host>—MatrixDSLHostis a non-actor wrapper that bootstraps a runtime and propagates the root context to children. Every Matrix page wraps content in this element.
Allowed APIs in components
You write standard browser code. The framework does not constrain you to a subset.
attachShadow({ mode: 'open' })is done by the base class.this.shadowRootis yours after construction.this.setState(patch)updates observable state and triggers a re-render if you implementrender(state).this.emit(event, payload)publishes to your$eventstopic.this.sendCommand(target, op, payload)(3-arg) sends to a child or relative path.this.sendCommand(op, payload)(2-arg) sends to a tracked-domain actor (CQRS view pattern; seestatic tracks).
What you do not do (these violate the actor model):
innerHTML = '<my-mx-thing>...'to create child Matrix components — that bypasses the$joinhandshake. Compose children instatic template, not viainnerHTML.- Direct method calls between sibling components. Always go through
sendCommandor by emitting an event the other component subscribes to. - Cross-package
imports of UI components. UI consumes its own backend through the bus, not through an in-process import.
The full ruleset is in projects/matrix-3/.claude/rules/component-development.md.
What the framework forbids in templates
The base class rejects unsafe patterns when it parses dynamic templates (MatrixActorHtmlElement.ts:152-173). The blocked patterns are:
- Inline event handlers (
onClick=,onLoad=, etc.) — usedata-action="myHandler"instead. javascript:URIs.eval(calls.new Function(calls.document.cookieaccess from a template.localStorage/sessionStorageaccess.- Direct
fetch(/XMLHttpRequest(/new WebSocket(. <script>,<iframe>,<object>,<embed>tags.srcdoc=attribute.- Dynamic
import(/require(.
These are enforced for templates supplied through factory.create-from-code and component.save. Static static template strings written by the package author are not parsed against this list — but the same restrictions are best practice.
Composability
Components compose by being placed in each other's static template. The parent shell discovers children via DOM walking (_findParentShell() and pending-children registration), waits for all of them to $join, propagates context, and continues bootstrap.
ts
class ParentShell extends MatrixActorHtmlElement {
static template = `
<my-widget id="left" name="left"></my-widget>
<my-widget id="right" name="right"></my-widget>
`;
}The name attribute is mandatory before DOM insertion. It becomes the child's id within the parent's ChildManager and the suffix on its mount path.
See also
- MatrixActorHtmlElement — class reference.
- UI component lifecycle — connect/disconnect/
_receiveContext. - Shadow DOM and rendering —
static templateandrender(state).
Source:
projects/matrix-3/packages/core/src/framework/MatrixActorHtmlElement.ts:152-173(sanitize patterns),:202-447(class declaration and registration),projects/matrix-3/.claude/rules/component-development.md(full rule set).