# Theming (/docs/theming)

microcharts theming is CSS custom properties. There's no theme provider to wire up and no Tailwind requirement: set a
`--mc-*` variable at any scope and every chart inside it retunes, with zero re-render and no build step. (Prefer a React
component to a raw attribute? [`MicroProvider`](#a-component-api-if-you-prefer) wraps the same contract.)

Two ideas carry the system. A **token** is one variable you set; a **scope** is where you set it. The rest of this page
is the vocabulary: every color, weight, and timing you can set, plus a studio that hands you any combination as
ready-to-paste CSS.

## Override a token

```css
.brand {
  --mc-accent: #7c3aed;
  --mc-stroke-width: 2;
}
```

Every chart inside `.brand` now draws with that accent and stroke weight. Precedence runs one way: a **prop** wins over
a **CSS-variable scope**, which wins over a **preset**, which wins over the built-in default.

```tsx
<Sparkline data={data} />
<Sparkline data={data} color="#7c3aed" />
<Sparkline data={data} color="var(--mc-positive)" />
```

## Scope it anywhere

Because the tokens cascade, you choose the scope: the whole app, one brand section, a single card, or one chart. Set a
token high and every chart below inherits it; override it lower and only that subtree changes.

```tsx
// app-wide, once
<html style={{ "--mc-accent": "var(--brand-500)" }}>

// just this card's charts
<div style={{ "--mc-stroke-width": 2, "--mc-gap": "0.15em" }}>
  <SparkBar data={weekly} />
</div>
```

Bind `--mc-accent` to your existing brand variable and every chart follows your design system without a per-chart prop
or a duplicated color value.

## Where the library's rules sit in the cascade

`styles.css` opens by declaring four cascade layers, and every rule it ships lives in one of them:

```css
@layer microcharts.tokens, microcharts.base, microcharts.charts, microcharts.motion;
```

Tokens are unaffected by this. You set a token on a scope of your own, the library never claims that scope, and your
value wins.

Overriding a **rule** works differently, because layer order beats specificity and the last layer declared wins. If your
app declares its own layers (Tailwind's `@layer theme, base, components, utilities`, or a framework that sets that up
for you) and `styles.css` is imported after them, the four microcharts layers are declared last and outrank your utility
classes. A class you add to a chart still lands on the element and still matches a real rule of yours. It loses the
cascade and paints nothing, with no warning anywhere.

Declare the order yourself to fix it. Put the microcharts layers first, above every import in your entry stylesheet, and
your own layers then win by being later:

```css
/* app entry CSS, before the imports */
@layer microcharts.tokens, microcharts.base, microcharts.charts, microcharts.motion, theme, base, components,
  utilities;
```

Importing `styles.css` before your framework's CSS reaches the same order with no layer statement, because imports
declare layers as they are parsed.

Either way, the order cuts both ways: once your layers outrank the library's, your **reset** outranks it too. Tailwind's
Preflight sets `svg { display: block }` in its `base` layer, which now beats the library's `display: inline-block` on
`.mc-root` — a chart in running text drops onto its own line until you put `inline-block` back at the call site. With
the reversed order you keep inline placement for free and lose your utilities instead. Pick per app: order your layers
and re-assert `inline-block` where a chart sits in prose, or keep the library last and reach for a token or `!important`
on the rule you need.

`!important` crosses the boundary, because importance inverts layer order: an important declaration in an earlier layer
beats an important one in a later layer, and both beat every normal declaration. Reach for it to unblock a single rule,
and reach for the layer statement above to stop hitting this at all.

Unlayered CSS never meets any of this. Plain rules beat every layered rule regardless of order, so an app with no
`@layer` of its own already wins.

## The token set

That's the whole mechanism; the rest is vocabulary. About two dozen custom properties cover the entire surface, and
setting any of them at any scope retunes every chart inside. Here are the color tokens with their built-in light and
dark values. Every chip is the shipped default; click one to copy the hex.

<TokenSwatches />

Below is the same set as raw CSS: the colors plus the geometry, motion, and readout-surface tokens, ready to paste and
override. Want a specific style or accent instead of the bare defaults? The [studio](#copy-tokens) further down builds
any combination for you.

```css
:root {
  /* Semantic color */

  /* Default ink: lines, bars, labels. Dark mode is hand-tuned, not inverted. */
  --mc-stroke: #1a1917;

  /* Good direction — color-blind-safe viridian; pair with ▲. */
  --mc-positive: #0e7a5f;

  /* Bad direction — color-blind-safe terracotta; pair with ▼. */
  --mc-negative: #bd4b2d;

  /* No-signal marks: baselines, inactive points, empty cells. */
  --mc-neutral: #8a8986;

  /* Emphasis color — rebind to your brand. */
  --mc-accent: #1f6091;

  /* Normal-range shading, derived from the ink. */
  --mc-band: color-mix(in oklab, var(--mc-stroke) 8%, transparent);

  /* MoonPhase lit area — warm amber; mono/print/eink retune it with the ink. */
  --mc-moon: #c1922f;

  /* Categorical palette — matte jewel tones; multi-series only, lightness-ordered.
     Each has a hand-tuned brighter twin on dark surfaces (same hue, lifted). */
  --mc-cat-1: #d2982f;
  --mc-cat-2: #5b9fd4;
  --mc-cat-3: #2e8c66;
  --mc-cat-4: #285788;
  --mc-cat-5: #bc5138;
  --mc-cat-6: #a55a89;

  /* Geometry */

  /* Light by default for inline use; bump to ~2 when standalone. */
  --mc-stroke-width: 1.5;

  /* Spacing between grouped marks; em-based so it scales with the font. */
  --mc-gap: 0.25em;

  /* Uniform density scale — compact (<1) vs comfortable (>1). Tunes stroke
     weight, label size, and small-multiple gap together; the box set by
     width/height is untouched. */
  --mc-density: 1;

  /* Direct-label text size, relative to the chart's font. */
  --mc-label-size: 0.75em;

  /* Direct-label weight. */
  --mc-label-weight: 400;

  /* `.mc-inline` seats marks on the text baseline (font-independent). This is
     an optional extra optical shift on top — 0 by default; set it only if a
     particular face wants marks a hair off the line. */
  --mc-inline-nudge: 0em;

  /* Extra optical shift for centred marks (arrow, dot, moon, dials, strips) on
     top of the cap-band seat — font-relative, negative lifts. 0em by default:
     the seat already lands on the true cap midpoint, so there is nothing left
     to correct unless a display face has an unusual cap ratio. */
  --mc-glyph-nudge: 0em;

  /* Motion & type */

  /* Motion duration — readout transitions, value pulses, entrance animation. */
  --mc-duration: 300ms;

  /* Ease-out entrance; no bounce or loop. */
  --mc-easing: cubic-bezier(0.22, 1, 0.36, 1);

  /* Inherits the host UI font + tabular numerals by default. */
  --mc-font: inherit;

  /* Face for figures + labels — tracks --mc-font; point at a brand or mono
     face to give numbers their own typeface without touching the page font. */
  --mc-font-numeric: var(--mc-font);

  /* Interactive readout chip (hover, focus, or a pinned selection).
     Defaults to system canvas. */
  --mc-surface: Canvas;
  --mc-surface-ink: CanvasText;
  --mc-surface-edge: color-mix(in oklab, CanvasText 16%, transparent);
}
```

## Semantic vs categorical color

The color tokens split into two groups, and the split decides what a theme is allowed to change.

**Semantic tokens keep their meaning.** Swapping `--mc-accent` retints the one emphasis color; `--mc-positive` and
`--mc-negative` still mean up and down. A preset can restyle a chart without changing what it says.

**Categorical colors are a last resort.** Most microcharts show one series and never touch the palette. When a chart
needs several, the six `--mc-cat-*` hues are lightness-ordered for grayscale and color-vision separation. Gold leads
because true yellow is too low-contrast on a light surface.

To recolor the series on _one_ instance without touching the global palette, the categorical charts (`SegmentedBar`,
`StackedArea`, `PartitionStrip`, `Hypnogram`, `MicroDonut`) take a `colors` array. It cycles per series and overrides
`--mc-cat-*` for that instance only. A rolled-up "Other" segment stays neutral instead of taking a series color.

```tsx
<SegmentedBar data={mix} colors={["#2563eb", "#db2777", "#65a30d", "#f59e0b"]} />
```

## Presets

A **preset** is a whole look bundled under one name, applied with a `data-mc-theme` attribute. It's pure CSS with zero
JavaScript, and it changes how charts look, never what the data means. `modern` is what you get with no attribute at
all: it's the base token set rather than a bundle layered on top, so it needs no attribute.

```tsx
<div><Sparkline data={data} dots="minmax" /></div> {/* modern — the default, no attribute */}
<div data-mc-theme="editorial"><Sparkline data={data} dots="minmax" /></div>
<div data-mc-theme="vivid"><Sparkline data={data} dots="minmax" /></div>
<div data-mc-theme="mono"><Sparkline data={data} dots="minmax" /></div>
```

What each preset retunes; every other token stays at its default:

<PresetDeltas />

The last two, `print` and `eink`, are output-context bundles rather than a look you'd ship on screen. `print` pins a
near-black ink, deepens the valence colors for CMYK, and thins the stroke to `1.25`. `eink` drops to grayscale and lets
sign ride lightness (positive black, negative gray) with a `2` stroke for a low-refresh e-paper panel. Apply them the
same way: `data-mc-theme="print"` on the region you're about to render. Both pin a fixed light-surface ink, so both also
ship hand-tuned dark twins. On a dark viewer `eink` flips to white ink on black and `print` lifts to a dark-paper ink,
while the stroke weight and band identity carry over.

The look presets (`editorial`, `mono`, `vivid`, `print`, `eink`) also answer to `data-mc-preset`, the page-global form:
set it once on `<html>` for an app shell, and a nearer `data-mc-theme` still wins by proximity. `dark` is the exception,
matching `data-mc-theme="dark"` only, because it's a color-scheme scope rather than a look you'd pin app-wide.

> Every preset above carries a **Copy tokens** button, and the [studio below](#copy-tokens) pairs any preset with your own accent and copies the result for both modes. This site's appearance menu (the palette icon in the nav) applies the same bundles live across every page. Picking an accent there re-hues every chart on the site, including the categorical palette, because it derives one with [`defineTheme`](#build-a-theme-from-one-colour) the way the studio does. It's the same token contract you'd use in your own app.

## Copy the tokens [#copy-tokens]

The studio puts everything above in one place. Pick a style, add your brand accent, choose a mode, and copy the `--mc-*`
block, light and hand-tuned dark together, ready to paste into your app. The preview renders real charts in both modes
and names each role it paints, so a choice shows up as you make it: the emphasis line takes your accent, and the
categorical bars re-hue to a matched palette **derived from it** by [`defineTheme`](#build-a-theme-from-one-colour), the
same engine the nav's appearance menu uses to re-theme the whole site. What you preview is what you copy, and what the
library ships.

<TokenStudio />

Paste the CSS into any stylesheet and every chart in scope retunes. Prefer the component API? Switch the format to a
tokens object and spread it onto [`MicroProvider`](#a-component-api-if-you-prefer), or call
[`defineTheme`](#build-a-theme-from-one-colour) directly to derive that matched palette in code.

## The MicroProvider component [#a-component-api-if-you-prefer]

Everything above works through plain attributes and CSS variables. If you'd rather set the theme in JSX, `MicroProvider`
wraps the same contract in a component. It's hook-free and RSC-safe, so it works in Server Components.

```tsx
import { MicroProvider } from "@microcharts/react";

<MicroProvider theme="editorial" tokens={{ "--mc-accent": "var(--brand-500)" }}>
  <Sparkline data={data} />
</MicroProvider>;
```

It renders a single `<div>` that sets `data-mc-theme` (the preset) plus any `tokens` as inline `--mc-*` custom
properties, identical to writing them by hand. `theme="modern"` writes no attribute, since modern _is_ the base token
set. `theme` takes any preset (`modern`, `editorial`, `mono`, `vivid`, `dark`, plus the output-context `print` and
`eink`); `tokens` takes any `--mc-*` overrides. Both forms resolve to the same cascade, so use whichever reads better in
your code.

## Build a theme from one color [#build-a-theme-from-one-colour]

Setting a token or two by hand is the common case. For a whole brand theme, with a matched categorical palette and
hand-tuned dark twins for every color, use `defineTheme` from `@microcharts/react/theme`. Give it your brand accent and
it derives the rest in OKLCH: a harmonized, color-blind-safe categorical palette and a lifted dark-mode twin for each
token. It never moves the positive/negative hues off their CVD-safe green/vermillion split, so a derived theme encodes
direction the way the defaults do.

```tsx
import { defineTheme } from "@microcharts/react/theme";
import { MicroProvider } from "@microcharts/react";

const brand = defineTheme({ accent: "#6d28d9" });

// spread the tokens onto one scope…
<MicroProvider style={brand.style}>
  <SegmentedBar data={mix} />
</MicroProvider>;

// …or emit a global stylesheet — includes a prefers-color-scheme: dark block
const css = brand.css(":root");
```

One accent, a full derived palette. These are the values `defineTheme` returns:

```tsx
const brand = defineTheme({ accent: "#6d28d9" });

<MicroProvider style={brand.style}>
  <SegmentedBar data={mix} title="Derived palette" />
</MicroProvider>
```

The result carries everything you need for any delivery: `vars` (a `--mc-*` object), `darkVars` (the derived dark twins,
empty when `dark: false`), `style` (an alias of `vars`, ready to spread onto `MicroProvider` or any element),
`css(selector = ":root")` (a string with the `prefers-color-scheme: dark` block included), `toString()` (the same as
`css(":root")`), and `extend()` to spin a variant off an existing theme. Extend a preset, pin any token, or pass an
explicit palette when you don't want derivation:

```tsx
// extend a preset — the brand accent flows through
defineTheme({
  extends: "editorial",
  accent: "var(--brand-500)",
});

// bring your own categorical palette (no derivation)
defineTheme({
  accent: "#6d28d9",
  cat: ["#2563eb", "#db2777", "#65a30d"],
});

// derive a specific number of categorical tones instead of the default six
defineTheme({ accent: "#6d28d9", cat: 3 });

// opt out of accent-seeded derivation, or out of dark twins entirely
defineTheme({ accent: "#6d28d9", derive: false });
defineTheme({ accent: "#6d28d9", dark: false });

// override just some dark twins (the rest stay auto-derived)
defineTheme({ accent: "#6d28d9", dark: { accent: "#a78bfa" } });

// compose a compact variant off an existing theme
defineTheme({ density: 0.85 }).extend({ labelWeight: 500 });

// pin geometry directly — strokeWidth, gap, labelSize, labelWeight, density
defineTheme({ accent: "#6d28d9", strokeWidth: 2 });
```

It's pure, dependency-free, and tree-shaken out of any bundle that never imports it.

## Density

`--mc-density` is a single scalar that scales stroke weight, label size, and small-multiple gap together: one knob for
compact tables (`< 1`) or an airier standalone layout (`> 1`). It tunes the ink and type, not the box. The plot size
still comes from `width`/`height`, so nothing reflows.

```css
/* a denser table of sparklines */
.metrics-table {
  --mc-density: 0.85;
}
```

One asymmetry: **stroke and gap scale without limit, but the label stops scaling at 1.25.** Direct labels sit in a
gutter the chart reserves from a character-count estimate while it draws, server-side, where text cannot be measured.
That reservation is fixed at render time and cannot see a CSS variable set later, so past about 1.25 the text outgrows
the space held for it and paints outside the chart. Below `1` there is no such limit: the label shrinks inside a gutter
already reserved for something bigger, which is safe.

So `--mc-density: 2` gives you double-weight strokes and double gaps, with labels at 1.25×. For larger text, scale the
chart with `width`/`height` or `--mc-label-size` instead of leaning on density.

## The font

`--mc-font` defaults to `inherit`, so charts adopt the font of whatever contains them. If the surrounding page sets a
`font-family`, charts follow it with no configuration. If it doesn't, as in some minimal starters and code sandboxes,
SVG text falls back to the browser default (usually a serif) and labels come out looking wrong.

Two fixes, either works: give your page a font the normal way, or point `--mc-font` at a stack for the charts alone.

```css
:root {
  /* charts only — the rest of the page is untouched */
  --mc-font: system-ui, sans-serif;
}
```

Numbers always render with `tabular-nums`, so columns of figures stay aligned in any font. Want the figures in a
dedicated face, a tabular numeric font or a mono for a data-dense KPI, without changing the label font? Point
`--mc-font-numeric` at it. It defaults to `--mc-font`, so leaving it alone keeps everything on one typeface.
`--mc-label-weight` (default `400`) sets label weight the same way.

```css
:root {
  --mc-font: Inter, sans-serif; /* labels */
  --mc-font-numeric: "IBM Plex Mono", monospace; /* figures */
}
```

## The readout surface

The one opaque plane in the library is the value chip an interactive chart shows for its active unit: under the pointer,
under keyboard focus, or pinned in place after a click, tap, or **Enter**. Its `--mc-surface`, `--mc-surface-ink`, and
`--mc-surface-edge` tokens default to the system `Canvas`, so the chip adapts to light, dark, and forced-colors with no
configuration. Point them at your popover tokens to match a themed surface. One set of tokens covers the hovered,
focused, and pinned states alike.

The fourth token, `--mc-surface-shadow`, stays a black mix in every theme, and that is deliberate. The chip renders in
the top layer over ground the library cannot know, so its drop shadow reads as occlusion rather than as part of the
palette, and occlusion is dark on a dark surface too. An earlier version mixed `CanvasText` the way the edge does, which
inverts with the theme and painted a white halo around the chip on dark. On dark the shadow now goes quiet and the 1px
`--mc-surface-edge` ring carries the separation. Override it if your surface needs a different depth; don't give it a
theme twin.

## The active state

The chip names the active unit; the chart also paints it. By default that mark is an accent hairline around the unit
plus a light wash inside it, and four tokens retune it. They are opt-in — the library declares none of them, so each one
falls back to the default in the comment:

```css
.my-charts {
  --mc-active-stroke: var(--mc-accent); /* ring color */
  --mc-active-fill: var(--mc-on-fill); /* wash inside the ring */
  --mc-active-fill-opacity: 0.2; /* its strength; 0 leaves a bare outline */
  --mc-rest-opacity: 1; /* every OTHER mark, while something is active */
}
```

The wash is why the ring reads on a mark the chart already emphasizes. SparkBar inks its endpoint bar with
`--mc-accent`, and an accent outline on an accent fill cancels out — so the outline alone left the most-looked-at bar in
the chart as the one that answered the pointer least.

Set `--mc-rest-opacity` below 1 and every mark that isn't the active one steps back while the pointer is down. The chart
is what knows which mark that is, so this is the one treatment you cannot write from the outside. Pair it with a solid
fill to lift the picked unit out of its own dimmed self:

```css
.my-charts {
  --mc-rest-opacity: 0.25;
  --mc-active-fill: var(--mc-accent);
  --mc-active-fill-opacity: 1;
}
```

Under forced colors all four collapse back to a system-accent outline: a themed ink was picked against your palette, not
the user's, and an opacity ramp has nothing to say in a two-ink mode.

## Dark mode and forced colors

Dark values are hand-tuned per token, never auto-inverted, so overriding one light value can't break the dark surface.
Charts never paint their own background, so they sit on whatever surface you give them.

The library reads the theme two ways. `prefers-color-scheme` is the default, and `data-mc-theme="light"` or
`data-mc-theme="dark"` on any scope pins that subtree regardless of the OS.

### Tell the charts which theme your app picked

If your app switches theme with a **class**, the charts do not see it. `.dark` on `<html>` is what next-themes,
fumadocs, and most Tailwind setups write, and it means nothing to a stylesheet that is reading `prefers-color-scheme`. A
reader on a light OS who picks dark mode gets your dark chrome with light-tuned charts on it, and a reader on a dark OS
who picks light gets the reverse. Both are legible enough to ship by accident and wrong enough to notice.

Set `data-mc-theme` wherever you set the class, and the two agree:

```tsx
<html className={theme} data-mc-theme={theme}>
```

That hands the charts the shipped dark bundle, hand-tuned token by token. If you'd rather paint them from your own
palette, rebind the tokens under your class instead. The library's defaults are `:where()` zero specificity, so your
class rules win:

```css
:root {
  --mc-accent: var(--brand);
}
.dark {
  --mc-stroke: #eae9e6;
  --mc-positive: #45a385;
  --mc-negative: #df7856;
  --mc-accent: var(--brand);
}
```

Rebinding by hand means rebinding `--mc-on-fill` too. It is the ink that reads on a saturated fill, it flips between
themes, and it is the one nobody restates: leave it on the dark branch under a light palette and the in-fill labels on
TraceFold, TimeInRange, and PartitionStrip drop to around 3.3:1 while every other token on the page is correct.
`data-mc-theme` carries it for you.

### Frosted or translucent surfaces

Default valence inks clear 4.5:1 on opaque white and near-black. On a frosted-glass or tinted panel the composited
backdrop is quieter, so deepen `--mc-stroke`, `--mc-positive`, `--mc-negative`, and `--mc-neutral` on that surface until
contrast holds. Rebind the tokens rather than patching chart CSS. (This docs site does that under `:root` and `.dark` so
marks stay legible on glass.)

Under Windows High Contrast (`forced-colors`), the semantic tokens defer to system colors: ink to `CanvasText`, accent
to `Highlight`, muted marks to `GrayText`. A chart stays legible even when your palette is overridden entirely.

`prefers-contrast: more` is handled too: charts bump `--mc-stroke-width` to `2` and deepen `--mc-band` from an 8% to a
16% mix of the ink, so lines and normal-range shading both gain weight without you configuring anything.
