Targ Apps Docs
Chroma

Architecture

How Chroma is structured - what runs in WebAssembly, where the JavaScript boundary is, and how a state change reaches the DOM.

Chroma's engine is Rust compiled to WebAssembly. It owns signals, dependency tracking, element creation, event wiring, and every DOM operation. The only JavaScript in the package is the loader generated by wasm-bindgen (pkg/chroma.js), which instantiates the .wasm when you import it.

Overview

How a state change reaches the DOM

  1. e("button", { onClick }, "Count: ", count) creates the element. The count getter becomes a binding bound to one text slot.
  2. While the binding evaluates, calling count() subscribes it to the signal.
  3. A click calls setCount(count() + 1). The runtime compares with Object.is, stores the new value, and synchronously re-runs the subscribed bindings.
  4. Each binding writes its result straight to its DOM target. No component function runs again and nothing is diffed.

The full mechanics are in Reactivity.

The WebAssembly boundary

Every DOM operation crosses from WebAssembly to JavaScript. This is a deliberate trade-off: the goal is an engine that lives entirely in Rust, not winning benchmarks against pure-JavaScript frameworks.

Browsers cannot import a .wasm file directly. ESM integration for WebAssembly is not shipped, and WebAssembly only exchanges numbers with JavaScript, so strings, objects, and callbacks need the wasm-bindgen bridge.

Error handling

  • Misuse of the public API (a non-Element container in render, an invalid tag in e, missing required props on Show or For) throws an error with a chroma: prefix.
  • Errors thrown inside your callbacks (effect bodies, cleanups, reducers, binding expressions, Provider children) are caught and logged to the console, so one broken callback does not take down the rest of the tree.
  • Unexpected internal panics are logged with the name of the component being rendered, followed by a readable stack trace.

There are no error boundaries. See Limitations.

Two routers

Chroma has two things called createRouter:

  • Screen router: exported by the WebAssembly engine (createRouter, matchRoute, parseQuery). Hash or history navigation between screens, with a reactive current() and a View component.
  • API router: exported by the @chroma/router plugin. File-based handlers under api/ that act as simulated endpoints in the browser.

See Routing for the API router and the API reference for the screen router.

The toolchain around the engine

PieceRole
CLIInstalls the toolchain, scaffolds apps, serves them in development, and builds them
Dev serverLoopback static server used by chroma dev
DesktopTauri shell that imports and runs chroma build bundles
Runtime modesPortable (web) vs executable (Desktop) — shared WASM, different hosts
PluginsBrowser JavaScript plugins loaded at startup (storage, styling, API routing)

Implementation details

The engine is organized into a pure dependency-graph module (no JavaScript or DOM awareness), a reactive runtime holding signal values and bindings, a DOM module for elements and events, and one module each for control flow, variants, the screen router, and the hooks. Control flow (Show, For) mounts content once and drives updates through an effect binding. The release profile is optimized for .wasm size; the loader's size is independent of it. Source: Chroma/src in the repository.

On this page