Targ Apps Docs
Chroma

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

PluginPackageWhat it doesDocs
chroma-fs@chroma/fsStorage (IndexedDB or filesystem)Chroma-Fs
chroma-router@chroma/routerAPI router for simulated endpointsRouting
chroma-style@chroma/styleDesign tokens, utility CSS, cva presetsStyling · Chroma-Style
chroma-t2t@chroma/t2tTest utilitiesChroma-t2t

Use a plugin in an app

  1. Add its name to plugins in chroma.json:

    {
      "name": "my-app",
      "mode": "dynamic",
      "plugins": ["chroma-fs", "chroma-router", "chroma-style"]
    }
  2. Run chroma new (new project) or chroma install then copy the plugin, so it exists under plugins/<name>/.

  3. 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:

  1. Fetches plugins/<name>/chroma.plugin.json.
  2. Imports the entry module.
  3. Calls the styles hook if declared (CSS injection).
  4. Calls the bootstrap hook if declared, and stores the result under loadAs. 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"
  }
}
FieldRequiredMeaning
nameYesFolder name in plugins/ and the value used in chroma.json plugins
packageYesnpm-style package id (@chroma/...)
entryYesMain module, relative to the plugin folder
loadAsYesKey on the object returned by loadPlugins()
hooksYesHook point mapped to the exported function name
capabilitiesNoTags for tooling
assetsNoStatic files, such as stylesheets
routingNoRouter-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

HookWhen it runsUsed by
bootstrapOnce, from loadPlugins()chroma-fs (bootstrapFs), chroma-router (bootstrapRouter), chroma-t2t (bootstrapT2t)
stylesBefore first paint, to inject CSSchroma-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 install stores the first-party plugins in ~/.chroma/plugins/.
  • chroma new copies every plugin listed in chroma.json into the project's plugins/ folder.
  • chroma build copies plugins/ into dist/.

The plugin code lives in your project, so you can read and patch it. See CLI for the commands.

Write your own plugin

  1. Create plugins/my-plugin/ with index.js, package.json, and chroma.plugin.json. Copy a first-party manifest as a starting point.
  2. Export the functions named in hooks, plus a plugin object with the same bindings.
  3. Add "my-plugin" to plugins in chroma.json.
  4. Run chroma dev and check that plugins.<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

On this page