A semantic light/dark theming standard and structural transformer for web SVG diagrams.
Install · Manifest standard · Outputs · Changelog · MIT license
Themed SVG gives diagram producers and site builders a shared contract for semantic SVG colors. A versioned manifest says what a color means and exactly which XML or CSS declaration owns it. The transformer safely produces SVGs that can follow the operating-system theme, inherit a host-page palette, or remain fully fixed.
The recommended web path is host-mode output loaded through <themed-svg>.
The runtime fetches and sanitizes the generated SVG, then inserts it into a
dedicated shadow root. Host CSS custom properties inherit into the SVG while
its IDs and embedded styles remain isolated.
The standard is diagram-generator neutral. Mermaid can consume it, but Mermaid does not own the architecture, manifest, CSS namespace, or transformer.
SVG theming has two separate layers:
-
Mode selection chooses light or dark. In standalone adaptive output this is
prefers-color-scheme; a host application may instead use its own state or manual toggle. -
Palette selection maps semantic roles such as
color.text.primaryto actual CSS colors.
Keeping these layers separate lets a build inject a site’s light and dark palettes without coupling diagrams to that site’s theme-switch implementation. The package includes conservative bundled light and dark defaults so every declared common token has a usable fallback.
Palette values merge in this exact order, from lowest to highest priority:
bundled defaults < manifest preset < manifest paletteOverrides.light or .dark < runtime shared palette < runtime light- or dark-specific palette
- Runtime-inlined host SVG (recommended)
-
Generate
hostoutput (the default) and load it with<themed-svg>. This supports live host variables and manual application theme toggles while keeping SVG IDs and styles inside a shadow root. - Standalone-adaptive external image
-
Choose
--mode standalone-adaptiveand use<img>when JavaScript is unavailable or unwanted. The SVG followsprefers-color-scheme, but cannot see host application state or custom properties. - Fixed or paired external images
-
Choose
fixedfor one concrete palette, orpaired-fixedfor separate light and dark files selected by the page. - Injection without the theming standard
-
If an existing SVG only needs to be fetched and inserted into the DOM for CSS styling, use an established injector such as
@iconfu/svg-inject.themed-svgis useful when the project also needs semantic manifests, palette merging, fixed/adaptive/host build outputs, or its stricter runtime contract.
An SVG loaded through <img src="diagram.svg"> is an isolated document.
Custom properties from the surrounding HTML page do not cross that boundary.
Use explicit standalone-adaptive for an <img> that must switch with the browser’s
light/dark preference, or generate paired-fixed files and let the page choose
the URL.
host output is intended for inline SVG. Its var() references can inherit
custom properties from the host document, including values controlled by a
manual theme toggle.
An adaptive SVG embedded through <img> cannot see a host application’s
class-, attribute-, or JavaScript-only manual toggle. It sees media preferences,
not host DOM state. Build-time site palette injection changes the colors inside
the standalone file, while mode selection still follows
prefers-color-scheme.
import { transformSvg } from '@dev-centr/themed-svg';
const result = transformSvg(sourceSvg, manifest, {
// host is the default
palette: sharedSitePalette,
lightPalette: siteLightPalette,
darkPalette: siteDarkPalette,
});
if (!result.svg) {
console.error(result.diagnostics);
}Bindings are authoritative. The transformer never guesses which duplicate
literal should change. discoverLiteralColors() reports literal colors only as
migration and diagnostics assistance.
The local package export is a self-contained browser ESM bundle. Importing it
registers <themed-svg>:
<script type="module">
import '@dev-centr/themed-svg/register';
</script>
<themed-svg
src="/diagrams/system.host.svg"
alt="System architecture"
description="Requests pass through the gateway to two services."
></themed-svg>A version-pinned npm CDN import can use:
<script type="module"
src="https://cdn.jsdelivr.net/npm/@dev-centr/themed-svg@0.2.0/browser/themed-svg-element.js">
</script>For an unreleased commit, use a commit-pinned GitHub URL after that commit contains the generated bundle:
<script type="module"
src="https://cdn.jsdelivr.net/gh/dev-centr/themed-svg@COMMIT/browser/themed-svg-element.js">
</script>Cross-origin sources are rejected by default. HTML cannot widen trust. Register explicitly from JavaScript when a known asset origin is required:
import { defineThemedSvgElement } from '@dev-centr/themed-svg/runtime';
defineThemedSvgElement({
trustedOrigins: ['https://assets.example.com'],
});The function API offers the same loader without automatic registration:
import { mountThemedSvg } from '@dev-centr/themed-svg/runtime';
const mount = mountThemedSvg(container, '/diagrams/system.host.svg', {
alt: 'System architecture',
});
await mount.loaded;The component supports src, alt, and description; reload() and
abort(); loading, loaded, and error state attributes; and load,
error, and abort events.
Keep documentation and third-party delivery portable by referencing a
standalone-adaptive SVG as an ordinary image. Mark images that an owned site
should upgrade after the runtime loads:
<img data-themed-svg src="/diagrams/system.svg"
alt="System architecture">
<script type="module">
import { upgradeThemedSvgImages } from '@dev-centr/themed-svg/register';
upgradeThemedSvgImages();
</script>The runtime derives /diagrams/system.host.svg, wraps the existing image, and
only hides that fallback after the sanitized host SVG loads. A missing script,
HTTP or MIME failure, rejected SVG, or unavailable host artifact leaves the
adaptive image visible. Set data-themed-svg-src when the host artifact does
not use the .host.svg sibling convention.
upgradeThemedSvgImage() upgrades one image. upgradeThemedSvgImages() scans
img[data-themed-svg] by default and safely ignores images already upgraded.
host(default)-
CSS-variable references with concrete fallbacks, but no media query or injected mode palette. Intended for the runtime component and host-controlled toggles.
standalone-adaptive-
Self-contained SVG with light defaults and a
prefers-color-scheme: darkpalette. Suitable for<img>. fixed-
One concrete palette. Contains no
var()references or theme media query. paired-fixed-
Separate concrete light and dark SVG strings/files. Both contain no variables or media query.
Store the editable diagram, manifest, and both delivery artifacts together:
system.mmd system.theme.json system.svg system.host.svg
Render Mermaid once to a temporary SVG, then transform that same render into both committed outputs:
mmdc -i system.mmd -o .generated/system.raw.svg -c mermaid-config.json
mermaid-svg-css-vars --manifest system.theme.json \
--mode standalone-adaptive .generated/system.raw.svg -o system.svg
mermaid-svg-css-vars --manifest system.theme.json \
--mode host .generated/system.raw.svg -o system.host.svgCI should repeat these commands and fail when either committed artifact differs. Fixed or paired-fixed exports may be retained for email, uploads, and consumers that cannot select adaptive or host output.
If an SVG has no viewBox, the transformer derives one only from positive
numeric width and height values. It does not invent dimensions. Missing
geometry produces a missing-viewbox error diagnostic and no output.
Version 1 is defined by
themed-svg-manifest-v1.schema.json.
It records namespace, source provenance, semantic tokens, presets, optional
light/dark palette overrides, explicit bindings, and fallback behavior.
{
"$schema": "https://docs.devcentr.org/themed-svg/schemas/themed-svg-manifest-v1.schema.json",
"schemaVersion": 1,
"namespace": "architecture-diagram",
"source": {
"kind": "diagram-generator",
"uri": "diagrams/system.mmd",
"generator": "mermaid"
},
"tokens": [
{ "id": "color.surface.primary", "description": "Primary node surface" },
{ "id": "color.text.primary", "description": "Primary readable text" }
],
"defaultPreset": "light",
"presets": {
"light": {
"color.surface.primary": "#f8fafc",
"color.text.primary": "#0f172a"
},
"dark": {
"color.surface.primary": "#1e293b",
"color.text.primary": "#f8fafc"
}
},
"paletteOverrides": {
"dark": { "color.surface.primary": "#111827" }
},
"bindings": [
{
"kind": "presentation",
"selector": "#service",
"attribute": "fill",
"token": "color.surface.primary"
},
{
"kind": "stylesheet",
"selector": ".label",
"property": "fill",
"token": "color.text.primary"
}
],
"fallback": {
"unresolvedToken": "error",
"missingTarget": "warn"
}
}Generated CSS custom properties use the independent namespace
--themed-svg-<manifest namespace>-<semantic token>, with dots normalized to
hyphens.
themed-svg --manifest diagram.theme.json diagram.svg > diagram.themed.svg
themed-svg --manifest diagram.theme.json \
--mode paired-fixed \
--palette shared.json \
--light-palette light.json \
--dark-palette dark.json \
--light-output diagram.light.svg \
--dark-output diagram.dark.svg \
diagram.svgRun themed-svg --help for all options. Diagnostics go to stderr. Exit code 2
means the transform reported an error and did not emit output.
Native editors can use the stable version 1 JSON or JSONL protocol without
writing temporary files. themed-svg-stdio defaults to JSONL and remains open,
writing one response for each input line:
themed-svg-stdio
# Equivalent:
themed-svg --stdio jsonlEvery request contains protocolVersion: 1, an optional string, number, or
null id, and one of these operations:
inspect-
Returns root geometry, element and stylesheet counts, sorted IDs, and literal color occurrences.
validate-
Validates an SVG, a version 1 manifest, or their bindings together when both are supplied.
transform-
Runs one canonical transform with the same options as
transformSvg(). sanitize-
Fail-closed validation plus canonical XML serialization. Unsafe input is rejected and produces no SVG rather than being partially repaired.
export-
Produces deterministic
standaloneAdaptive,host,fixed,light, anddarkartifact strings by default. Restrict work withoptions.modes.
{"protocolVersion":1,"id":"preview-42","operation":"export","svg":"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"40\" height=\"20\">...</svg>","manifest":{"schemaVersion":1,"namespace":"preview","tokens":[],"defaultPreset":"light","presets":{"light":{},"dark":{}},"bindings":[]},"options":{"modes":["standalone-adaptive","host"]}}Responses echo protocolVersion, id, and operation, then provide ok,
result, and diagnostics. Diagnostics can include bindingIndex, selector,
and source context (kind, JSON Pointer path, line, and column). Request
errors are protocol responses, so JSONL processing continues. Process failure
is reserved for CLI or stream failure.
For a single request followed by process exit, use:
themed-svg --stdio json < request.json > response.jsonThe TypeScript envelopes and processStdioRequest() are available from
@dev-centr/themed-svg/protocol. The protocol schema is exported as
@dev-centr/themed-svg/schema/stdio-v1.
The transformer parses XML with saxes and @xmldom/xmldom, and parses CSS
with postcss and postcss-value-parser. It rejects DOCTYPE declarations,
scripts, foreignObject, SMIL mutation, event-handler attributes, external
links/resources, CSS imports, external CSS URLs, and injectable palette values.
It validates the completed output again after all generated changes.
The browser runtime applies DOMPurify’s SVG profile and additional fail-closed
DOM and CSS checks. It accepts same-origin HTTP(S) by default, verifies redirect
destinations, requires successful image/svg+xml responses, limits declared
and received bytes, and never inserts untrusted strings with innerHTML.
Trusted origins are an explicit JavaScript-only allowlist and do not bypass
sanitization.
Selectors deliberately support a safe, deterministic subset. Explicit presentation attributes, inline declarations, stylesheet declarations, and gradient stops can be bound. Existing IDs and unbound duplicate literals remain unchanged.
pnpm install
pnpm build
pnpm test:unit
pnpm exec playwright install chromium
pnpm test:browser
pnpm packThe package is independently packable and has no Mermaid runtime dependency.
See CHANGELOG for the release timeline and detailed change records.
The owned-organization migration ledger records the diagram inventory, canonical source chains, exclusions, and verification gate used for the initial rollout.
Themed SVG is available under the MIT License.
Use the GitHub issue tracker for bugs, standard proposals, and interoperability questions.