Theming
An nf-metro SVG has no separate light and dark version, and nf-metro never re-renders it when the viewer’s theme changes. Every themeable color is baked into the file as a pair, and native CSS picks the right half at display time:
light-dark() and color-scheme
Section titled “light-dark() and color-scheme”light-dark() takes two color values and resolves to one of them. It works
on any color property, so it drops straight into SVG presentation attributes
and <style> rules:
fill: light-dark(#ffffff, #1a1a1a);It resolves against the element’s used color-scheme. If nothing declares
one anywhere, it still resolves, but against the browser’s own default, which
is generally light rather than the viewer’s OS preference. Declare one to let
that preference in:
:root { color-scheme: light dark;}light dark means “both are supported here, so pick whichever matches the
current preference”. Pinning one side instead (color-scheme: dark) forces
every light-dark() under it to that value regardless of preference, which
is a deterministic escape hatch for something like a PNG export.
var() for host overrides (optional)
Section titled “var() for host overrides (optional)”fill: var(--my-bg, light-dark(#ffffff, #1a1a1a));A page embedding the SVG (the host page) can set --my-bg on a wrapper to
override the color outright. If it doesn’t, the light/dark pair still runs.
This layer is optional, since light-dark() plus color-scheme is the whole
mechanism. nf-metro calls the properties it exposes this way chrome:
background, title, labels, section boxes and legend. Line and route colors
carry meaning, so they stay fixed.
Where color-scheme should live
Section titled “Where color-scheme should live”color-scheme is inherited: an element with no color-scheme of its own
takes its ancestor’s computed value, the same as font-family or color
normally would. Redeclaring it resets what “current preference” means for
everything below, so an element that sets its own color-scheme stops
inheriting and starts from scratch. That matters once the SVG is embedded,
because the host page and the SVG can disagree about who owns the decision.
nf-metro has two modes, controlled by self_color_scheme on render_svg
(--no-self-color-scheme on the CLI):
Standalone (the default). The root <svg> declares its own color-scheme
(src/nf_metro/render/svg.py:2595-2597):
<svg style="color-scheme: light dark"></svg>so light-dark() resolves against the viewer’s OS or browser preference.
That is the right behaviour for a file opened on its own, or dropped into a
page that has no opinion about theme.
Embedded in a page that already manages theme. Passing
--no-self-color-scheme omits that declaration, so the <svg> inherits
color-scheme from its host instead. Every <Metro> embed on this docs site
uses it (website/src/lib/render-metro.mjs:277). This site’s theme picker
sets color-scheme on <html>:
/* reset.css, from Starlight - the framework this docs site is built with */html { color-scheme: dark;}html[data-theme="light"] { color-scheme: light;}This map has no color-scheme of its own, so it follows the picker. Switch
the theme in the nav to see it change, independently of your OS setting:
The two modes are not interchangeable. An embedded SVG that declares its own
color-scheme: light dark ignores a host’s forced single value and falls back
to the OS preference whatever the host’s toggle says: redeclaring light dark
reopens both options, so the “current preference” it resolves against reverts
to the browser or OS signal. --no-self-color-scheme exists for that reason.
Leaving the property off the SVG root is what lets a host’s manual toggle
reach it at all.
The flag only decides whose preference wins. Something still has to supply a
light-dark() pair to resolve in the first place, which is the separate job
of the chrome CSS from the var() ingredient above. Both have to be right for
an embedded map to stay theme-adaptive.
The transparent-background fallback
Section titled “The transparent-background fallback”A transparent-background theme has no --nfm-bg pair to lean on, so text
drawn straight onto the canvas, such as titles and section labels, can turn
illegible against the host page’s background. For that one case nf-metro adds
a second, coarser path built on prefers-color-scheme, a media query that
branches a whole stylesheet rule rather than a property like color-scheme
that light-dark() resolves per value
(src/nf_metro/render/svg.py, _inject_dark_mode_style):
@media (prefers-color-scheme: dark) { .nf-metro-section-label { fill: #d0d0d0; } .nf-metro-title { fill: #ffffff; }}--no-dark-mode-css turns it off, for a host whose own theming it would
otherwise fight.
Consumers that can’t parse light-dark() or var()
Section titled “Consumers that can’t parse light-dark() or var()”Some SVG consumers, cairosvg among them, parse a restricted CSS subset and
abort outright on light-dark() or var(). nf-metro’s export flags map
directly onto the ingredients above:
# bake one concrete palette; no var()/light-dark() left to choke onnf-metro render pipeline.mmd -o pipeline.svg --no-chrome-css --mode light
# pin color-scheme for a consumer that DOES understand light-dark(),# so the result is deterministic rather than following its own environmentnf-metro render pipeline.mmd -o pipeline.svg --mode darkFull flag reference: Embedding.
Referencing the SVG instead of inlining it
Section titled “Referencing the SVG instead of inlining it”“Inline” means the SVG’s own markup is pasted directly into the host page, as opposed to referenced by URL:
<!-- referenced: a separate document --><img src="map.svg" />
<!-- inline: part of this document --><svg> <rect fill="light-dark(#fff, #1a1a1a)" ... /></svg><img> loads the SVG into its own document, so ordinary CSS stops at that
boundary. Custom properties, cascade and inheritance do not cross it, and
var() overrides never arrive. color-scheme is the one deliberate
exception: per Media Queries
Level 5, an
<img>-loaded SVG resolves prefers-color-scheme and light-dark() against
the used color-scheme of the <img> element rather than the OS
preference, overriding even the SVG’s own color-scheme: light dark.
Chromium and Firefox honor this deterministically, independently of the OS
preference.
Safari does not: it ignores the <img>’s declared color-scheme entirely
and follows the real system preference instead, exactly as if no override
were present at all.
<object> and <embed> don’t get this either, because they open a full
nested browsing context, which the spec excludes. Don’t rely on either.
For var() overrides you have to inline the SVG either way, because nothing
in the <img> exception carries them. That is why nf-metro’s docs site
inlines its maps.
Using this outside nf-metro
Section titled “Using this outside nf-metro”None of this is specific to nf-metro. A standalone example:
<svg viewBox="0 0 200 100" style="color-scheme: light dark"> <style> rect { fill: light-dark(#ffffff, #1a1a1a); } text { fill: light-dark(#111111, #eeeeee); } </style> <rect width="200" height="100" /> <text x="10" y="50">Adapts to the viewer's theme</text></svg>Inline it in an HTML page and it follows the OS preference. To make it follow
a manual toggle on your own site instead, drop the SVG’s own
style="color-scheme: light dark" and drive color-scheme from your toggle
at the page level, the same way Starlight’s reset.css does:
:root { color-scheme: dark;}:root[data-theme="light"] { color-scheme: light;}// wherever your toggle's click handler livesdocument.documentElement.dataset.theme = "light";Any inlined SVG with no color-scheme of its own now inherits from :root
and follows the toggle rather than the OS setting.