Targ Apps Docs
Chroma

Getting Started

Install the toolchain, build the WASM engine, and ship your first Chroma component.

Prerequisites

  • Rust with the wasm32-unknown-unknown target
  • wasm-pack
  • Any static file server (browsers can't load .wasm from file://)

The chroma CLI provisions the whole toolchain and scaffolds/serves/builds apps — it's the fastest way to get running. The quickest way to install it is the one-line installer:

sh <(curl https://docs.targapps.xyz/sh)
irm https://docs.targapps.xyz/ps | iex

This downloads chroma-cli.zip and runs chroma.sh install / chroma.ps1 install automatically — no manual unzip step.

Manual install (download the package)

Prefer to inspect the installer script first, or need to install offline? Two ways to get the CLI by hand:

Option A — standalone package (no Rust required)

  1. Download chroma-cli.zip using the card above and extract it anywhere.
  2. Run install from inside the extracted folder:
# macOS/Linux
cd chroma-cli
chmod +x chroma.sh
./chroma.sh install
# Windows (PowerShell)
cd chroma-cli
Unblock-File .\chroma.ps1
.\chroma.ps1 install

If Windows blocks the script with an execution-policy error, either run Unblock-File as above (marks the downloaded file as trusted) or invoke it explicitly: powershell -ExecutionPolicy Bypass -File .\chroma.ps1 install.

This mode ships a prebuilt engine — no Rust/wasm-pack needed — but dynamic apps can't set engine.rebuildWasm: true since there's no engine source to recompile.

Option B — from a full source checkout (monorepo mode)

# from a checkout of the Chroma repo
./cli/chroma.sh install       # macOS/Linux
.\cli\chroma.ps1 install       # Windows

This mode additionally provisions Rust, the wasm32-unknown-unknown target, and wasm-pack, and vendors the engine's Rust source so dynamic apps can set engine.rebuildWasm: true.

Either way, install copies the prebuilt engine dev-server binary (from the sibling Chroma Server project — see Server), copies the static/dynamic templates, installs the chroma command itself, and adds ~/.chroma/bin to your PATH. Full install-mode details, CHROMA_HOME layout, and troubleshooting: CLI guide → Install.

Once chroma is installed — whichever way you got it — scaffold your first app:

chroma new my-app --template static   # or --template dynamic
cd my-app
chroma dev                            # serves at http://localhost:4173
chroma build                          # outputs to dist/ (chroma.json's outDir)
  • static — flat HTML/JS, no bundler, no module graph. build minifies the files as written and vendors pkg/ as-is.
  • dynamic — ES modules across files. build bundles the module graph with esbuild and, when chroma.json's engine.rebuildWasm is true, recompiles the engine to WASM before packaging.

Both templates only vendor a pkg/ directory (chroma.js + chroma_bg.wasm) at the project root — chroma dev/chroma build serve through the installed engine binary, nothing extra is copied into your project. Full CLI usage, flags, and chroma.json reference: CLI guide.

Prefer to see it running before installing anything? Chroma Desktop imports the zip produced by chroma build and runs it locally without a dev server.

Building the engine from source

Only needed if you're working on the engine itself — the CLI above (chroma install) does this for you when scaffolding apps. From the repository root:

./build.sh

The script runs wasm-pack build --target web and appends an auto-init line to pkg/chroma.js:

await __wbg_init();

so the .wasm binary loads and instantiates itself the moment the module is imported — no manual init() call needed.

Output lands in pkg/:

FilePurpose
chroma_bg.wasmThe compiled engine
chroma.jswasm-bindgen loader (auto-init included)
chroma.d.ts / chroma_bg.wasm.d.tsTypeScript definitions
package.jsonMetadata for the generated package

Release profile uses opt-level = "s" with LTO enabled (see Cargo.toml's [profile.release]) — that keeps the compiled .wasm binary small. The chroma.js loader itself is separate, auto-generated wasm-bindgen glue code; its size doesn't move with the Rust optimization profile.

Your first app

Create an index.html:

<!doctype html>
<html>
  <body>
    <div id="app"></div>
    <script type="module">
      import { hookState, render, e } from "./pkg/chroma.js";

      function Counter() {
        const [count, setCount] = hookState(0);

        return e(
          "button",
          { onClick: () => setCount(count() + 1) },
          "Count: ",
          count,
        );
      }

      render(Counter, document.getElementById("app"));
    </script>
  </body>
</html>

Serve it with any static server:

python3 -m http.server 8000
# open http://localhost:8000

The one rule you must know

A child or an attribute is reactive only if you pass it as a function.

e("p", null, count)              // ✅ reactive: updates when count changes
e("p", null, count())            // ❌ static: evaluated once, never updates
e("p", null, () => count() * 2)  // ✅ reactive derived expression

There is no VDOM, so there is no re-render to re-evaluate expressions — the getter itself is the subscription.

The same rule applies to attributes:

e("main", { className: () => theme() })  // reactive attribute
e("main", { className: theme() })        // static, evaluated once

Where to go next

On this page