Getting Started
Install the toolchain, build the WASM engine, and ship your first Chroma component.
Prerequisites
- Rust with the
wasm32-unknown-unknowntarget - wasm-pack
- Any static file server (browsers can't load
.wasmfromfile://)
Install the CLI (recommended)
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 | iexThis 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)
- Download
chroma-cli.zipusing the card above and extract it anywhere. - Run
installfrom 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 installIf 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 # WindowsThis 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.
buildminifies the files as written and vendorspkg/as-is. - dynamic — ES modules across files.
buildbundles the module graph with esbuild and, whenchroma.json'sengine.rebuildWasmistrue, 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.shThe 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/:
| File | Purpose |
|---|---|
chroma_bg.wasm | The compiled engine |
chroma.js | wasm-bindgen loader (auto-init included) |
chroma.d.ts / chroma_bg.wasm.d.ts | TypeScript definitions |
package.json | Metadata 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:8000The 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 expressionThere 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 onceWhere to go next
- API Reference — every exported function
- Hooks Guide — effects, memos, context, reducers
- Reactivity Model — how it works underneath
- CLI — full
chromacommand reference