Theming
How to theme microcharts by setting a --mc-* custom property at any scope, applying a named preset, or deriving a whole palette from one accent with defineTheme().
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 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
.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.
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.
// 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.
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.
--mc-strokeDefault ink — lines, bars, labels--mc-positiveGood direction — pair with ▲--mc-negativeBad direction — pair with ▼--mc-neutralNo-signal marks, baselines--mc-accentEmphasis — rebind to your brand--mc-bandNormal-range shading, derived from ink--mc-moonMoonPhase lit area — warm amberGold--mc-cat-1Azure--mc-cat-2Emerald--mc-cat-3Sapphire--mc-cat-4Terracotta--mc-cat-5Mauve--mc-cat-6Below 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 further down builds any combination for you.
: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.
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.
What each preset retunes; every other token stays at its default:
The default — nothing to override.
Hairline ink, a signature claret endpoint.
One ink — direction carries by shape, never colour.
Bolder ink and punchier valence; accent stays yours.
Paper output — near-black ink, hairline weight, valence deepened for CMYK.
Grayscale e-paper — sign rides lightness, heavier strokes for low refresh.
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 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 the way the studio
does. It's the same token contract you'd use in your own app.
Copy the 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, 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.
:root { --mc-stroke: #1a1917; --mc-positive: #0e7a5f; --mc-negative: #bd4b2d; --mc-neutral: #8a8986; --mc-accent: #2f52d4; --mc-band: color-mix(in oklab, var(--mc-stroke) 8%, transparent); --mc-moon: #c1922f; --mc-cat-1: #6b87cb; --mc-cat-2: #8c548c; --mc-cat-3: #c26e65; --mc-cat-4: #876701; --mc-cat-5: #5a9b62; --mc-cat-6: #007a86;}@media (prefers-color-scheme: dark) { :root { --mc-stroke: #eae9e6; --mc-positive: #45a385; --mc-negative: #df7856; --mc-neutral: #9a9a97; --mc-accent: #528dff; --mc-moon: #e0be6f; --mc-cat-1: #95b2fa; --mc-cat-2: #b87db8; --mc-cat-3: #f1998f; --mc-cat-4: #b2903e; --mc-cat-5: #85c78c; --mc-cat-6: #23a8b7; }}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, or call
defineTheme directly to derive that matched palette in code.
The MicroProvider component
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.
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
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.
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:
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:
// 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.
/* 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.
: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.
: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 three --mc-surface* 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.
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.
microcharts follows prefers-color-scheme by default, and you can force a scope with data-mc-theme="dark". Charts
never paint their own background, so they sit on whatever surface you give them.
If your app toggles theme with a class (next-themes .dark, a [data-theme] attribute, …) rather than the OS media
query, rebind the tokens under that class the same way you bind accent — the library's media-query defaults are
:where() zero-specificity, so your class rules win:
:root {
--mc-accent: var(--brand);
}
.dark {
--mc-stroke: #eae9e6;
--mc-positive: #45a385;
--mc-negative: #df7856;
--mc-accent: var(--brand);
/* …or data-mc-theme="dark" on a scope, if you prefer the shipped dark bundle */
}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.
Annotations
Thresholds, target zones, markers, and callouts are written as children and drawn in the host chart's own coordinate space.
Accessibility
How microcharts names, announces, and navigates a chart: generated summaries, the decorative opt-out, the keyboard and live-region contract, color and locale, and system preferences.