Browser plugins by mode
Portable (Web) vs Executable (Chroma Desktop) behavior for chroma-fs, chroma-router, chroma-style, and chroma-t2t.
First-party plugins are browser ESM packages vendored under plugins/<name>/. Runtime modes change the host around them, not the plugin manifest format. Registration is always:
- List the id in
chroma.json→plugins. - At startup,
plugins/loader.jsfetches eachchroma.plugin.json, importsentry, runshooks.styles/hooks.bootstrap, and returnsloadPlugins()keys (fs,router,style,t2t).
The loader passes router bootstrap options from options.router (default manifest ./api/routes-manifest.json). Everything else uses plugin defaults unless your app wraps loadPlugins({ ... }).
Summary table
| Plugin | loadAs | Hooks | Portable (Web) | Executable (Desktop) | Primary entry points |
|---|---|---|---|---|---|
chroma-fs | fs | bootstrap → bootstrapFs | Full with IndexedDB (default) | Full; auto-defaults to filesystem when platform is desktop | chroma-fs.json, detectPlatform(), iframe detection |
chroma-router | router | bootstrap → bootstrapRouter | Full | Full | hijackFetch, api/routes-manifest.json |
chroma-style | style | styles → injectStyles | Full | Full | App calls bootstrapStyle({ cva, cx }) |
chroma-t2t | t2t | bootstrap → bootstrapT2t | Partial | Partial | exposeGlobal, runNodeTests (Node only) |
chroma-fs
Package: @chroma/fs. Docs: Chroma-Fs, Backends.
Portable (Web)
| Capability | Status |
|---|---|
readFile / writeFile / list with default config | Full via IndexedDB |
storage.backend: "indexeddb" | Full |
storage.backend: "filesystem" | Fails in a normal top-level tab — throws unless you pass a custom adapter (tests) or embed in a host that provides one |
| Data survives refresh | Full for the same origin (URL + port) |
| Sharing data with Desktop | Different partition — export/import manually |
Platform detection (detectPlatform()): no __TAURI__, not in Desktop iframe → "static" → default backend indexeddb when storage.backend is omitted.
Executable (Chroma Desktop)
| Capability | Status |
|---|---|
| App in library iframe | Full — iframe is treated as "desktop" even without Tauri globals in the child |
| Default backend when omitted | filesystem (postMessage bridge to shell) |
storage.backend: "filesystem" | Full via chroma-fs channel on window.parent |
storage.backend: "indexeddb" | Full but keyed to loopback origin (47823 or dev port) |
Develop mode (chroma dev + Desktop) | Different origin from browser --no-desktop dev — separate IndexedDB |
| On-disk paths | Under <app-data>/apps/<app-id>/data/ — see Desktop storage |
Entry points:
chroma-fs.json— explicitstorage.backend,basePath, IndexedDB names.createStorage({ adapter })— forces filesystem with in-memory or test adapters (any mode).- Host: Chroma Desktop
index.htmlparent handles{ channel: "chroma-fs", op, ... }.
chroma-router
Package: @chroma/router. Docs: Routing.
Both modes: Full
| Behavior | Portable | Executable |
|---|---|---|
Load api/routes-manifest.json | Yes | Yes |
Register routes from api/**/<METHOD>.js | Yes | Yes |
Hijack globalThis.fetch for /api/* | Yes | Yes |
| Real network to your backend | Only if URL does not match local routes | Same |
The router does not detect Desktop. It runs entirely in the iframe JavaScript context. Simulated endpoints are not a production API server in either mode.
Entry points:
chroma.plugin.json→routing.manifestPath(default./api/routes-manifest.json).loadPlugins({ router: { manifestUrl, hijackFetch: false } })to disable fetch hijack.globalThis.__CHROMA_DEBUG__for verbose logging (both modes).
chroma-style
Package: @chroma/style. Docs: Styling, Chroma-Style.
Both modes: Full
| Behavior | Portable | Executable |
|---|---|---|
injectStyles() loads tokens.css, utilities.css | Yes | Yes |
bootstrapStyle({ cva, cx }) presets | Yes | Yes |
Theme via data-theme / CSS variables | Yes | Yes |
| Dependency on host file APIs | None | None |
The styling pipeline is pure DOM + CSS. No compile flag switches style between Portable and Executable.
Entry points:
- Plugin hook
styles→injectStyles(loader runs before bootstrap). - App entry must call
bootstrapStyleonce with enginecva/cx(templates insrc/main.chr). - FOUC and
<link>ordering: Chroma-Style — Runtime hosts.
chroma-t2t
Package: @chroma/t2t. Docs: Chroma-t2t.
Both modes: Partial (by design)
The plugin is loaded in the browser context in Portable and Desktop. Node APIs are not available inside the WASM/ESM bundle.
| API | Portable (Web) | Executable (Desktop) |
|---|---|---|
bootstrapT2t / globalThis.__chromaT2t | Full | Full |
smokeSnapshot / assertPersistRoundTrip (configured adapters) | Full | Full — persist tests follow your storage adapters (often Fs-dependent) |
runNodeTests / runAll({ includeNodeTests: true }) | Skipped (process undefined) | Skipped — run npm test or node plugins/chroma-t2t/node/run-tests.mjs on the dev machine |
| Production bundles | Usually omit plugin from plugins | Same recommendation |
Persist round-trip tests validate app-provided adapters (export/import/flush), not the host mode directly. When adapters use live Chroma-Fs, round-trip behavior differs between IndexedDB and filesystem backends — configure tests per runtime mode.
Entry points:
bootstrapT2t({ persist, snapshot, node, exposeGlobal, globalKey }).options.node.command/npmTestScriptfor documented CLI test entry (runs outside the page).
WASM screen router (not a plugin)
Exported from pkg/chroma.js, not plugins/.
| Feature | Portable | Executable |
|---|---|---|
createRouter, matchRoute, hash/history | Full | Full |
| Interaction with API router | Orthogonal — screen vs /api fetch | Same |
Legacy libraries
Not plugins: entries in chroma.json legacyLibraries and <script> tags in index.html. chroma add vendors files; dev / build do not rewrite HTML automatically.
| Behavior | Portable | Executable |
|---|---|---|
| CDN or vendored UMD/globals | Full | Full |
| Mode-specific loading | N/A — same HTML in dist/ | Same zip import |
Editor support for legacy libs is limited; see Legacy libraries.
Custom plugins
Your chroma.plugin.json follows the same contract in both modes:
- Avoid Node built-ins and native modules.
- If you need disk, delegate to
@chroma/fsor feature-detectdetectPlatform()from@chroma/fsrather than calling Tauri from the iframe. - Use
capabilitiesfor tooling tags only; nothing in the loader branches on Portable vs Executable.
Related
- Plugins guide — manifests and hooks
- Extensions overview — master matrix
- Runtime modes
Extensions & plugins by mode
How each Chroma plugin, host, and editor tool behaves in Portable (Web) vs Executable (Chroma Desktop), and which entry points change internals.
Editor & CLI tooling
How the VS Code extension, linter, and Chroma CLI relate to Portable (Web) vs Executable (Chroma Desktop) — project-side, not runtime hosts.