The one mechanism you must understand: --fg → currentColor
system.css computes ink for every element (@layer core.color { :where(*) { … color: oklch(…) } }),
and bridges SVG with :where(svg) { color: currentColor }. So the recipe is always:
- Set
--fg(and--hue-shiftwhen you want color) on an SVG element or a<g>. - Paint with
fill="currentColor"and/orstroke="currentColor".
That's it. The engine turns --fg into the actual color, and currentColor picks it up.
The --fg scale — n ∈ [-1 .. 0 .. 1]
--fg encodes contrast and chroma in one number. Sign picks the channel; magnitude is strength:
-1→ heavy neutral ink (grayscale, maximum contrast vs. the surface).0→ equals the background (invisible — useful for nothing, avoid).+1→ maximum chromatic contrast (fully saturated ink at the current hue).
Negative is neutral/structural; positive is colored/expressive. So chart furniture (axes, gridlines, ticks, titles) is negative, and data marks (bars, bubbles, lines) are positive when you want them to carry hue. A practical ladder:
| Layer | --fg |
why |
|---|---|---|
| Faint gridlines | -0.13 |
barely there, just enough to read values |
| Axis lines | -0.4 |
structural but quiet |
| Labels (foLabel default) | -0.8 at --type: -2 |
readable, clearly furniture |
| Data marks | 1 |
full chroma — the data is the star (gf-69, per Mike) |
| Data marks (neutral) | -0.85 |
a single-series chart that wants no hue |
| Quiet outlines (a silhouette behind the data) | -0.25 |
the unselected states/counties, at low fill-opacity (gf-364) |
| A prior period | -0.55 dashed |
last year's line under this year's (gf-347) |
| A secondary fill under the marks | 0.2 |
the territory's states under its counties at 1 (gf-364) |
(These are the SHIPPED values — the Metrics charts, the Charts shelf, the landing and the territory picker use exactly these rungs; don't invent intermediate steps.)
Hue per series: --hue-shift, never --hue-lock
THE LADDER (gf-182, per Mike — supersedes the full-wheel spread
below): series step +30° apiece from the theme hue — an analogous
run that stays cohesive with the page and re-themes as one family; the
wheel spread read as a carnival against any theme. Fixed step, not
divided by count (series four keeps its color when a fifth arrives).
hueShift/hueStyle implement it; the shelf's "Categorical color"
card shows the ladder re-theming live under a second surface hue.
The engine's hue is --hue + --hue-shift (unless --hue-lock overrides it). To give
each series its own color, spread them around the wheel and set --hue-shift on the
mark: shift = idx * 360 / n. Because it rides on the inherited --hue, the whole
chart re-themes correctly when the page hue changes — admin-red vs. product-indigo just
works.
Never reach for --hue-lock to color a series. --hue-lock hard-pins hue for an
element and its entire subtree and refuses the cascade — it exists only for semantic
primitives (success/info/danger pills) that must mean the same thing everywhere. Using
it as a coloring tool poisons inheritance and is, per the color API's load-bearing
rules, insurmountable tech debt. If you just want a color, that's --fg (+ --hue-shift).
The ONE sanctioned hue-lock use in a chart: a segment whose color IS a meaning —
created = success, rejected = danger (view/statusbar.go's result bar). That is the
semantic-primitive case the lock exists for, not series coloring.
Text via <foreignObject> — and its scaling trap
ALL chart text is engine-typed <foreignObject> — the foLabel helper in
view/chartrender.go (2026-08-30, per Mike: no SVG <text>, no hand fonts,
no px literals; the engine types everything). ⚠ The text lives in SVG
coordinates: the whole drawing — text included — scales by
rendered-width / viewBox-width, so --type: -2 only means what it says
when the viewBox width ≈ the rendered width. Pin the builder's nominal W
to its host container and say so in a comment (view/statusbar.go's "THE
NOMINAL WIDTH IS THE DIALOG'S" warning is the exemplar).