Skip to content

Repository files navigation

Contributors Forks Stars Issues License

Themed SVG

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.

Why two theming layers matter

SVG theming has two separate layers:

  1. 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.

  2. Palette selection maps semantic roles such as color.text.primary to 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

Web delivery choices

Runtime-inlined host SVG (recommended)

Generate host output (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-adaptive and use <img> when JavaScript is unavailable or unwanted. The SVG follows prefers-color-scheme, but cannot see host application state or custom properties.

Fixed or paired external images

Choose fixed for one concrete palette, or paired-fixed for 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-svg is 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.

Installation

pnpm add @dev-centr/themed-svg

Node.js 20 or later is supported.

API

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.

Runtime component

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.

Progressive external-image upgrade

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.

Output modes

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: dark palette. 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.

Reproducible dual-output convention

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.svg

CI 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.

Manifest standard

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.

CLI

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.svg

Run 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 editor stdio protocol

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 jsonl

Every 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, and dark artifact strings by default. Restrict work with options.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.json

The 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.

Safety and structural processing

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.

Development

pnpm install
pnpm build
pnpm test:unit
pnpm exec playwright install chromium
pnpm test:browser
pnpm pack

The package is independently packable and has no Mermaid runtime dependency.

Changelog

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.

License

Themed SVG is available under the MIT License.

Contact

Use the GitHub issue tracker for bugs, standard proposals, and interoperability questions.

About

Semantic light/dark theming standard and structural transformer for web SVG diagrams.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages