Plugins
Enable, load, and write Chroma plugins, including the chroma.plugin.json manifest, hook points, and how plugins are vendored into your project.
Chroma plugins are browser-side ESM packages that live in your project's plugins/<name>/ folder. You list them in chroma.json, the CLI copies them in, and plugins/loader.js initializes them at startup. They must run in the browser: no Node APIs, no native modules.
For Portable (Web) vs Executable (Chroma Desktop) behavior per plugin (storage, reload, t2t limits), see Extensions & plugins by mode and Runtime modes.
The same loader runs in Portable (Web) tabs and in Executable (Chroma Desktop) iframes. Plugin entry points and hooks are shared; side effects (for example where @chroma/fs persists data) follow the runtime mode. For a row-by-row breakdown, see Extensions & plugins by mode.
Built-in plugins
| Plugin | Package | What it does | Docs |
|---|---|---|---|
chroma-fs | @chroma/fs | Storage (IndexedDB or filesystem) | Chroma-Fs |
chroma-router | @chroma/router | API router for simulated endpoints | Routing |
chroma-style | @chroma/style | Design tokens, utility CSS, cva presets | Styling · Chroma-Style |
chroma-t2t | @chroma/t2t | Test utilities | Chroma-t2t |
Use a plugin in an app
-
Add its name to
pluginsinchroma.json:{ "name": "my-app", "mode": "dynamic", "plugins": ["chroma-fs", "chroma-router", "chroma-style"] } -
Run
chroma new(new project) orchroma installthen copy the plugin, so it exists underplugins/<name>/. -
Call the loader before rendering:
// src/main.chr import { render, cva, cx } from "../pkg/chroma.js"; import { loadPlugins } from "../plugins/loader.js"; import { App } from "./App.chr"; const plugins = await loadPlugins(); if (plugins.style) { globalThis.__chromaStyle = plugins.style.bootstrapStyle({ cva, cx }); } render(App, document.getElementById("app"));
loadPlugins() returns an object keyed by each plugin's loadAs value (plugins.fs, plugins.router, plugins.style, plugins.t2t).
How the loader works
The loader in the dynamic and standard templates is generic. For each name in chroma.json plugins it:
- Fetches
plugins/<name>/chroma.plugin.json. - Imports the
entrymodule. - Calls the
styleshook if declared (CSS injection). - Calls the
bootstraphook if declared, and stores the result underloadAs. If there is no bootstrap hook, the module itself is stored.
A plugin without chroma.plugin.json is skipped with a console warning, except chroma-fs, which has a legacy fallback.
chroma.plugin.json
Every plugin has this manifest at its root:
{
"name": "chroma-router",
"package": "@chroma/router",
"version": "0.1.0",
"description": "HTTP-like route handlers for Chroma apps (browser JS, not Node).",
"entry": "index.js",
"loadAs": "router",
"capabilities": ["routing", "fetch-intercept"],
"hooks": {
"bootstrap": "bootstrapRouter"
}
}| Field | Required | Meaning |
|---|---|---|
name | Yes | Folder name in plugins/ and the value used in chroma.json plugins |
package | Yes | npm-style package id (@chroma/...) |
entry | Yes | Main module, relative to the plugin folder |
loadAs | Yes | Key on the object returned by loadPlugins() |
hooks | Yes | Hook point mapped to the exported function name |
capabilities | No | Tags for tooling |
assets | No | Static files, such as stylesheets |
routing | No | Router-only settings (manifestPath, reservedMethods, pathConvention) |
Validate the manifest with the chroma.plugin.schema.json JSON Schema by adding it as $schema:
"$schema": "https://docs.targapps.xyz/schemas/chroma-plugins/chroma.plugin.schema.json"The same folder also publishes schemas for chroma-fs.json and the routes manifest.
Hook points
| Hook | When it runs | Used by |
|---|---|---|
bootstrap | Once, from loadPlugins() | chroma-fs (bootstrapFs), chroma-router (bootstrapRouter), chroma-t2t (bootstrapT2t) |
styles | Before first paint, to inject CSS | chroma-style (injectStyles) |
chroma-style declares only styles. Call bootstrapStyle({ cva, cx }) yourself from the app entry, as shown above.
Each first-party plugin also exports a default plugin object (name, capabilities, hooks) for generic loaders.
Install and vendoring
chroma installstores the first-party plugins in~/.chroma/plugins/.chroma newcopies every plugin listed inchroma.jsoninto the project'splugins/folder.chroma buildcopiesplugins/intodist/.
The plugin code lives in your project, so you can read and patch it. See CLI for the commands.
Write your own plugin
- Create
plugins/my-plugin/withindex.js,package.json, andchroma.plugin.json. Copy a first-party manifest as a starting point. - Export the functions named in
hooks, plus apluginobject with the same bindings. - Add
"my-plugin"topluginsinchroma.json. - Run
chroma devand check thatplugins.<loadAs>is defined. The generic loader needs no change unless your plugin requires special bootstrap options.
// plugins/my-plugin/index.js
export function bootstrapMyPlugin() {
return { hello: () => "hi" };
}
export const plugin = {
name: "my-plugin",
hooks: { bootstrap: bootstrapMyPlugin },
};
export default plugin;{
"name": "my-plugin",
"package": "@chroma/my-plugin",
"entry": "index.js",
"loadAs": "myPlugin",
"hooks": { "bootstrap": "bootstrapMyPlugin" }
}Next
- Runtime modes — when the same plugin API maps to different storage or origins
- Extensions & plugins by mode — master matrix per plugin and host
- Chroma-Style and Chroma-t2t — first-party plugin deep dives
- Quick start
- CLI —
chroma buildcopiesplugins/intodist/ - VS Code extension — validates
chroma.plugin.jsonin the editor