How to automate css custom property injection across svg libraries
- Step 1Export a colour-to-token map from your token source — From Style Dictionary, Tokens Studio, or a hand-maintained JSON, produce
{ "#003082": "--color-brand-primary", "#e02d3c": "--color-brand-accent" }. Normalise every hex to lowercase 6-digit form for reliable matching. - Step 2Fetch the tool schema —
GET /api/v1/tools/svg-css-variable-injector(with your API key) returns the contract, including thevariableMapoption (string-array, default[]) andexecution.runnerMode: "engine". - Step 3Pair a local @jadapps/runner — Install and pair the runner (see
/docs/runner). The public/runendpoint never accepts uploads — it returns the runner endpointhttp://127.0.0.1:9789/v1/tools/svg-css-variable-injector/run. Files are processed locally. - Step 4Convert your map to the `variableMap` array — Flatten the JSON into
["#003082:--color-brand-primary", "#e02d3c:--color-brand-accent"]. Remember: any entry disables auto-detect, so include every colour you want turned into a variable. - Step 5POST each SVG to the runner — For each file, POST
{ input: "<svg>…</svg>", options: { variableMap: [...] } }to the runner. The response is the rewritten SVG text with each mapped hex replaced byvar(--token). - Step 6Emit the CSS and commit — From the same token map, write
:root { --color-brand-primary: #003082; … }to atokens.css. Commit the rewritten SVGs and the CSS together as one atomic change so the icons and their declarations stay in sync.
API / runner contract
Exact endpoints and option for automating the injector. JAD's API never receives file content — execution is local via the runner.
| Concern | Value |
|---|---|
| Schema endpoint | GET /api/v1/tools/svg-css-variable-injector |
| Runner endpoint | POST http://127.0.0.1:9789/v1/tools/svg-css-variable-injector/run |
| Run mode | engine (server-safe, no headless browser) |
| Option | variableMap — string-array of "#hex:--varName", default [] |
| Output | Rewritten SVG text only (no CSS file) |
| Minimum tier | pro |
Behaviour the pipeline must account for
Engine semantics derived from the implementation. Script around these, not against them.
| Behaviour | Detail | Implication for your loop |
|---|---|---|
| Map disables auto-detect | If variableMap has ≥1 entry, only those colours are mapped; the rest stay hardcoded | List every colour you want parameterised; don't expect leftovers to auto-name |
| Empty map → auto-name | An empty variableMap assigns --svg-color-N to each distinct #hex in first-appearance order | Only use empty maps when generic names are acceptable; names aren't stable across files |
| Supplied hex is expanded | Engine normalises your hex (#03f→#0033ff, lowercased, trimmed) before matching | Pass any case/length; but the SVG must contain the colour for the match to land |
| Literal text match | Replacement is a case-insensitive literal find of the hex in the SVG source | Normalise source SVGs to 6-digit hex first if they use shorthand or rgb() |
-- auto-prefix | A varName without leading -- gets it added | Either form works in your map strings |
| No CSS emitted | Output is the SVG only | Generate :root declarations from your own token map |
Cookbook
Pipeline snippets. The runner returns SVG text; your script writes the CSS from the same token map.
Convert a Style Dictionary colour map to variableMap
Flatten a hex→token object into the "#hex:--token" string array the tool expects.
const tokens = {
"#003082": "--color-brand-primary",
"#e02d3c": "--color-brand-accent",
"#1a1a2e": "--color-surface"
};
const variableMap = Object.entries(tokens)
.map(([hex, name]) => `${hex}:${name}`);
// ["#003082:--color-brand-primary", ... ]POST one SVG to the local runner
The public API returns pairing instructions; the runner on localhost does the work. Artwork stays on your machine.
curl -s http://127.0.0.1:9789/v1/tools/svg-css-variable-injector/run \
-H 'content-type: application/json' \
-d '{
"input": "<svg viewBox=\"0 0 24 24\"><path fill=\"#003082\" d=\"...\"/></svg>",
"options": { "variableMap": ["#003082:--color-brand-primary"] }
}'
# → <svg ...><path fill="var(--color-brand-primary)" d="..."/></svg>Batch a whole icon directory
Loop over the library, reuse one map, and write each result back. Deterministic output keeps git diffs clean.
for (const file of glob('icons/**/*.svg')) {
const svg = read(file);
const out = await runner('svg-css-variable-injector', {
input: svg,
options: { variableMap }
});
write(file.replace('icons/', 'icons-themed/'), out);
}Generate the :root CSS from the same map
The tool doesn't emit CSS, so derive it from your token map — single source of truth, no drift.
const css =
':root {\n' +
Object.entries(tokens)
.map(([hex, name]) => ` ${name}: ${hex};`)
.join('\n') +
'\n}\n';
write('tokens.css', css);
// :root { --color-brand-primary: #003082; ... }CI sync step on token changes
Re-run injection whenever the token file changes and open a PR with the regenerated SVGs + CSS together.
# .github/workflows/theme-sync.yml (sketch)
on:
push:
paths: ['tokens/colors.json']
jobs:
inject:
steps:
- run: node scripts/inject-svg-vars.js # uses the runner
- run: node scripts/emit-tokens-css.js
- uses: peter-evans/create-pull-request@v6Edge cases and what actually happens
Public /run endpoint returns 400 with pairing instructions
400 runner requiredPOSTing content to https://…/api/v1/tools/svg-css-variable-injector/run intentionally returns HTTP 400 with runnerEndpoint, schemaEndpoint, and installRunner fields. JAD's API never accepts uploads — point your loop at the local runner on 127.0.0.1:9789 instead.
Map present but a colour wasn't in the file
Expected (no-op for that entry)An entry whose hex doesn't appear in the SVG simply matches nothing — no error, no change. Useful for a shared map across heterogeneous icons, but it means a typo'd hex fails silently. Diff the output to confirm replacements landed.
Source uses 3-digit shorthand
Match riskThe engine expands your supplied hex (#03f→#0033ff) but compares against the raw SVG text. If the file stores #03f, a #0033ff map entry won't match it. Normalise source SVGs to 6-digit hex (e.g. through svg-hex-swapper) as a pre-step in the pipeline.
Forgot a colour — it stays hardcoded
By designBecause any variableMap entry turns off auto-detect, colours absent from your map are left as literal hex. This is correct for protecting fixed brand colours, but it means an incomplete map yields partially-themed icons. Validate that every intended colour is in the map.
Named or rgb() colours in the library
Not handledThe injector targets #hex only. Icons using fill="red" or rgb(...) won't be parameterised. Add a normalisation pass before injection, or exclude those assets and handle them separately.
Expecting a CSS file from the API
By designThe run output is the rewritten SVG text — there is no generated .css in the response. Your pipeline must emit :root declarations from the token map (see the cookbook). Treat the token map as the single source for both the SVG injection and the CSS.
Same hex used for different semantic tokens
LimitationIf two semantically different colours happen to share a hex (#000000 used for both text and a border), a single map entry rewrites both to the same variable. The tool can't distinguish intent — disambiguate at the design-token level before injecting.
Rate limit hit on the schema endpoint
429 rate limitedThe schema GET is rate-limited per API key. In a large batch, fetch the schema once and cache it; the per-file work goes to the local runner, which is not rate-limited by JAD. Respect the X-RateLimit-* headers on the schema call.
Frequently asked questions
What's the exact option I pass?
variableMap: an array of "#hex:--varName" strings, e.g. ["#003082:--color-brand-primary", "#e02d3c:--color-brand-accent"]. It defaults to []. An empty array triggers auto-naming (--svg-color-N); any entries switch to explicit mapping only.
Where does the file actually get processed?
On your machine. GET /api/v1/tools/svg-css-variable-injector returns the schema, but execution runs through a paired @jadapps/runner on http://127.0.0.1:9789/v1/tools/svg-css-variable-injector/run. The public /run endpoint returns 400 with pairing instructions — JAD's API never receives uploads.
Does the API also return the CSS variable declarations?
No. The run output is the rewritten SVG text only. Generate the :root { --token: #hex; } block from the same token map your loop already uses — that keeps the SVGs and CSS in sync from one source.
Does passing a map turn off auto-detection?
Yes. With any variableMap entry, only the listed colours are mapped and all other colours stay hardcoded. Auto-naming (--svg-color-N) happens only when the map is empty. List every colour you want parameterised.
Do I need to normalise hex case or length?
The engine lowercases, trims, and expands shorthand on your supplied hex before matching, so the map side is forgiving. The risk is on the source side: the SVG must literally contain the colour. Standardise your SVGs to 6-digit lowercase hex for reliable hits.
Is this tool headless-browser or engine mode?
Engine mode. It's in the server-safe set, so the runner executes it in-process without spinning up a headless browser. That makes it fast and CI-friendly compared to the Canvas/ZIP tools that require a browser session.
How do I integrate with Tokens Studio for Figma?
Tokens Studio exports JSON. Map your colour tokens to a { hex: varName } object, flatten it to the "#hex:--name" array, and feed that as variableMap. The same object generates your :root CSS — one export drives both.
Will it preserve SVGs that already use var()?
Yes. The tool replaces literal hex strings; an existing var(--something) contains no hex, so it's untouched. Re-running injection on already-themed files only affects any remaining hardcoded hexes in your map.
How do I keep icons in sync when tokens change?
Add a CI job triggered on changes to your token file: re-derive the map, re-run injection through the runner, regenerate tokens.css, and open a PR with both. Because the transform is deterministic, unchanged icons produce no diff noise.
What tier does the automation require?
The tool's minimum tier is pro. API access and runner pairing are gated by your account/plan; check /docs/runner and your API key's scopes. SVG file-size limits also apply per tier (Pro 50 MB), though icons are well under that.
Can I parameterise gradient and filter colours in batch?
Yes — stop-color and flood-color carry hexes, so any map entry matching those hexes rewrites them to var(--name). No special option is needed; it falls out of the literal hex replace.
How do I handle icons with named or rgb() colours in the batch?
Pre-normalise them to hex (e.g. with svg-hex-swapper) before the injection step, or route them to a separate path. The injector only detects and maps #hex values.
Privacy first
Every JAD SVG tool runs entirely in your browser using the DOM API and Canvas. Your SVG files never leave your device — verified by zero outbound network requests during processing.