Developer Docs

This documentation is still being finalized and may change as the product evolves.

This page stays practical: install, configure, connect a file, bootstrap Path A or Path B, apply contexts, and export CSS. If you want the short mental model first, start with How it works. If you are handing off from Figma, use Designer Docs.

1. Prerequisites

  • A frontend that can import an npm package (Next.js, Remix, Vite React/Vue, …)
  • TokenIgnite Figma plugin streaming for the file (designer signed in)
  • The TokenIgnite file id from the plugin (e.g. tokenignite-096149) — not a Figma file key
  • UI already using var(--…) where you want live updates — TokenIgnite injects variables; it does not rewrite components
Terminal
npm i -D tokenignite
npx tokenignite init
npx tokenignite --help

init only writes tokenignite.config.json. It does not install the package.

2. Configuration

JSON
{
  "attributeNames": {
    "context": "data-ti-context",
    "active": "data-ti-active"
  },
  "bridgePort": 5678,
  "files": [
    {
      "id": "tokenignite-096149",
      "rootSelector": ":root",
      "name": "global-foundation",
      "exportPath": "./app/styles"
    }
  ]
}
FieldPurpose
attributeNames.contextActive design contexts (default data-ti-context)
attributeNames.activeActive stream name (default data-ti-active)
bridgePortLocal CLI ↔ browser port for session status only. Live tokens still stream directly into the browser.
files[].idTokenIgnite file id (literal) or "process.env.VAR_NAME"
files[].rootSelectorWhere runtime CSS applies (e.g. :root)
files[].nameName for tokenignite run <name> and as data-ti-active value
files[].exportPathExport directory → {exportPath}/{name}.css

Prefer env references for files[].id in public repos; literals are fine in private repos.

3. How live updates reach the browser

  1. Figma plugin publishes onto the live sync stream.
  2. Browser SDK connects after a one-time public bootstrap.
  3. SDK injects/updates a runtime <style> tag with CSS custom properties.
  4. Optional CLI (tokenignite run <name>) mirrors tokens in the terminal and can export. The bridge port is not for token delivery.

4. Client bootstrap — two paths

Path A — Local: initTokenIgnite() + CLI run

Needs an active tokenignite run <name>. The bridge only reports that the session is alive. initTokenIgnite takes optional { bridgePort } only (default 5678) — not the full config. File id and stream name come from the CLI session.

TypeScript
// Vite: if (import.meta.env.DEV) {
if (process.env.NODE_ENV === "development") {
  import("tokenignite").then(({ initTokenIgnite }) => initTokenIgnite());
  // optional: initTokenIgnite({ bridgePort: 5678 })
}
Terminal
tokenignite run global-foundation

After starting the CLI: click your app’s browser tab once (or switch away and back). Path A often waits for that focus before connecting — avoids console noise while no run is active.

Path B — Closed staging: runTokenIgnite(target, config)

Closed staging = production-built, access-controlled (not public production). No terminal run. Do not wrap in NODE_ENV === "development" — that would disable TokenIgnite on a production-like staging deploy.

Pass files[].name, a literal file id, or an env value the bundler can see:

TypeScript
import config from "../tokenignite.config.json";
 
import("tokenignite").then(({ runTokenIgnite }) =>
  runTokenIgnite("global-foundation", config)
);
 
// or: runTokenIgnite("tokenignite-096149", config);
// or: runTokenIgnite(process.env.NEXT_PUBLIC_FILE_ID, config);

Env refs in config resolve in the browser only when the bundler exposes them (e.g. NEXT_PUBLIC_*). Otherwise pass the resolved id as target. Keep Path B off public production via deploy/access control.

5. Terminal UI (tokenignite run <name>)

In-place dashboard. Compact pages keep scrollback; tall pages (especially CSS) expand for native scroll.

Terminal
[TokenIgnite | Public Beta] Run Mode | Name: global-foundation | File-ID: tokenignite-096149 | Variables: 128
⚠  12 number variables have Figma scopes that do not resolve to a unique unit → rendered unitless (fallback).

Live Feed (last 100 changed CSS variables):
  [CHANGED]  spacing:default  --spacing-gap-2xl: 32px;
  [CHANGED]  color:light  --color-brand-accent: #f2e932;

Run Mode (2 = View Contexts, 3 = View CSS, 4 = Exit Run Mode & Export CSS, 5 = Exit Run Mode):
KeyPage
1Live Feed (default) — last 100 changes ([ADDED] / [CHANGED] / [RENAMED] / [DELETED])
2Contexts — copy-paste collectionName:modeName for data-ti-context
3CSS — full live export preview
4Exit and export to {exportPath}/{name}.css
5Exit without export

Variables in the header = unique Figma variable entities (same idea as Figma collection totals), not one row per mode value.

6. Contexts & CSS variables

Figma collections/modes become normalized kebab-case segments (leading 1- prefixes and special characters stripped). CLI page 2 and plugin filters show the same labels.

HTML
<html data-ti-context="color-modes:dark-mode">
<html data-ti-context="color-modes:dark-mode spacing-scale:comfortable">

Multiple contexts are space-separated. Matching uses CSS ~=.

TypeScript
export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" data-ti-context="color-modes:dark-mode spacing-scale:comfortable">
      <body>{children}</body>
    </html>
  );
}

Optional: follow the system color scheme

TokenIgnite does not emit prefers-color-scheme queries. Drive modes yourself with the exact labels from CLI page 2:

JavaScript
const mq = window.matchMedia("(prefers-color-scheme: dark)");
 
function applySystemContext() {
  document.documentElement.setAttribute(
    "data-ti-context",
    mq.matches ? "color-modes:dark-mode" : "color-modes:light-mode"
  );
}
 
applySystemContext();
mq.addEventListener("change", applySystemContext);

CSS variable names

Without WEB code syntax: --{group-path}-{variable-name} (collection/mode not in the name). Explicit WEB syntax in Figma wins.

CSS
--spacing-gap-2xl: 32px;

7. Export CSS vs live injection

Live validation injects CSS for the active stream and sets data-ti-active to files[].name.

Exported CSS uses Smart Cascade: values identical across a collection’s modes lift into one :is(…[data-ti-context~="…"]…) common layer; only real per-mode diffs stay in explicit blocks. The :not([data-ti-active="<name>"]) guard prevents export rules from fighting live injection for that same stream.

CSS
:is(
  :root:not([data-ti-active="global-foundation"])[data-ti-context~="color-modes:light-mode"],
  :root:not([data-ti-active="global-foundation"]) [data-ti-context~="color-modes:light-mode"],
  :root:not([data-ti-active="global-foundation"])[data-ti-context~="color-modes:dark-mode"],
  :root:not([data-ti-active="global-foundation"]) [data-ti-context~="color-modes:dark-mode"]
) {
  --space-md: 1rem;
}
 
:root:not([data-ti-active="global-foundation"])[data-ti-context~="color-modes:light-mode"],
:root:not([data-ti-active="global-foundation"]) [data-ti-context~="color-modes:light-mode"] {
  --surface-container: #f5f5f5;
}
 
:root:not([data-ti-active="global-foundation"])[data-ti-context~="color-modes:dark-mode"],
:root:not([data-ti-active="global-foundation"]) [data-ti-context~="color-modes:dark-mode"] {
  --surface-container: #211f26;
}

Exported CSS is unlayered. With cascade layers:

CSS
@import "./tokens.css" layer(theme);

Optional: strip data-ti-active guards in production CSS (Vite)

Keep the guards in the committed export so teammates’ live validation still works. This snippet only strips them from Vite’s production emit — it does not rewrite the file on disk:

JavaScript
import { defineConfig } from "vite";
 
export default defineConfig(({ mode }) => ({
  css: {
    postcss: {
      plugins:
        mode === "production"
          ? [
              {
                postcssPlugin: "remove-ti-active",
                Rule(rule) {
                  if (rule.selector.includes("data-ti-active")) {
                    rule.selector = rule.selector.replace(/:not\(\[data-ti-active=.*?\]\)/g, "");
                    rule.selector = rule.selector.replace(/\s+/g, " ").trim();
                  }
                },
              },
            ]
          : [],
    },
  },
}));

8. End-to-end checklist

  1. Plugin streaming; copy TokenIgnite file id
  2. npm i -D tokenignitenpx tokenignite init → set files[].id / name
  3. Path A: bootstrap only when NODE_ENV === "development". Path B: no that guard; keep off public production
  4. Path A: tokenignite run <name> → click the app tab once → header shows name, file id, Variables count
  5. Change a Figma variable → browser style tag updates; Live Feed shows [CHANGED] / [ADDED]
  6. Set data-ti-context from Contexts page → mode overrides apply
  7. Press 4 to export when ready

9. Keep the SDK updated

Terminal
npm install tokenignite@latest

If your install is below the server minimum, connect is blocked and the console tells you to upgrade.

10. Troubleshooting

SymptomFix
Upgrade required / cannot connectnpm install tokenignite@latest
No live updatesPlugin streaming? Correct files[].id and name? Path A has tokenignite run running?
initTokenIgnite waits foreverStart tokenignite run <name>; match bridgePort
Run is up, but no styles in the browserClick the app tab once (Path A focus reconnect); keep CLI run active
Env id empty in browserExpose with NEXT_PUBLIC_* (or equivalent), or pass a resolved id to runTokenIgnite
UI does not changeInspect the injected <style> tag; components must use matching var(--…) / Figma code syntax
TokenIgnite in public productionPath A: keep behind NODE_ENV === "development". Path B: closed staging only. Prefer devDependency.
Port busyChange bridgePort or free the port

11. Accounts

  • Consuming a live stream locally or on closed staging does not require a developer TokenIgnite account.
  • You still need the designer’s file id and an active plugin stream.
  • Workspace membership (sharing inside the product UI) is separate and may require sign-in even if you already know the file id.