Core concepts
Theming
Theme tokens, the --ic-* CSS variables, presets and createTheme. Charts read shadcn/ui variables by default.
A theme is two sets of tokens, one for light and one for dark. Any colour token may be a literal (#2a78d6, oklch(...)) or a CSS variable reference (var(--brand, #2a78d6)). Variables are resolved against the chart's own DOM node, so scoped overrides work and the canvas always receives concrete colours.
Try the presets and accents. Everything below re-themes through one InsightProvider. Some presets are designed for a background colour of their own, and the demo paints it. The last eight names are the poster themes. See presets.
Subscription revenue rose from $42m in Q1 to $63m in Q4
Revenue by line of business
$ millions per quarter
import { useRef, useState } from "react";
import { BarChart, BigNumber, InsightProvider, LineChart, useColorMode } from "insightcharts";
import { createTheme, posterBackgrounds, themes, themeSurfaces } from "insightcharts/themes";
const revenue = [
{ quarter: "Q1", Subscriptions: 42, Services: 18, Hardware: 9 },
{ quarter: "Q2", Subscriptions: 48, Services: 19, Hardware: 11 },
{ quarter: "Q3", Subscriptions: 55, Services: 21, Hardware: 10 },
{ quarter: "Q4", Subscriptions: 63, Services: 20, Hardware: 14 },
];
const accents = ["#2a78d6", "#0f766e", "#7c3aed", "#c2410c"];
export default function Theming() {
const [preset, setPreset] = useState("insight");
const [accent, setAccent] = useState<string | null>(null);
// A preset is a complete theme; createTheme layers a partial one over it.
const base = themes[preset] ?? themes.insight;
const theme = accent ? createTheme({ accent }, base) : base;
// Presets other than insight and shadcn are designed for a surface of their own, and poster
// themes for a painted ground: paint it.
const box = useRef<HTMLDivElement>(null);
const { mode } = useColorMode(box, "auto");
const grounds = { ...themeSurfaces, ...posterBackgrounds } as Record<string, { light: string; dark: string } | undefined>;
const surface = grounds[preset]?.[mode];
const chip = { padding: "4px 12px", borderRadius: 999, border: "1px solid var(--border)", fontSize: 13 };
return (
<div ref={box} style={{ display: "grid", gap: 20 }}>
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" }}>
{Object.keys(themes).map((name) => (
<button key={name} type="button" aria-pressed={preset === name} onClick={() => setPreset(name)} style={{ ...chip, background: preset === name ? "var(--accent)" : "transparent" }}>
{name}
</button>
))}
<span style={{ width: 1, height: 20, background: "var(--border)", margin: "0 4px" }} />
{accents.map((color) => (
<button key={color} type="button" aria-label={`Accent ${color}`} aria-pressed={accent === color} onClick={() => setAccent(accent === color ? null : color)} style={{ width: 22, height: 22, borderRadius: 999, background: color, outline: accent === color ? "2px solid var(--foreground)" : "none", outlineOffset: 2 }} />
))}
</div>
<InsightProvider theme={theme}>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(260px, 1fr))", gap: 28, alignItems: "start", background: surface, borderRadius: 12, padding: surface ? 20 : 0 }}>
<div style={{ display: "grid", gap: 20 }}>
<BigNumber value={97} format="currency:USD" label="Revenue, Q4 ($m)" delta={0.128} deltaLabel="vs Q3" context="Subscriptions were 65% of the total" />
<BarChart data={revenue} x="quarter" y="Subscriptions" format="{value}m" height={170} title="Subscription revenue rose from $42m in Q1 to $63m in Q4" />
</div>
<LineChart data={revenue} x="quarter" y={["Subscriptions", "Services", "Hardware"]} format="{value}m" height={300} title="Revenue by line of business" subtitle="$ millions per quarter" note="Illustrative data" />
</div>
</InsightProvider>
</div>
);
}
How the default theme follows your page
With no configuration, the chrome follows the host page. The default theme reads shadcn/ui's variables when they exist and falls back to neutral values when they do not:
| Token | Reads | Fallback (light / dark) |
|---|---|---|
surface | --background | #fcfcfb / #1a1a19 |
ink | --foreground | #0b0b0b / #f0efec |
muted | --muted-foreground | #898781 |
fontFamily | inherited from the page | system UI font |
Series colours do not come from the page by default. The default categorical order is checked for separation between neighbours under colour-vision deficiency in both modes, so keep the order if you re-skin it.
CSS variables
Every token of the default theme can be overridden with an --ic-* variable, set anywhere above the chart: on :root, on a section, or on a single wrapper.
Default tokens
Scoped --ic-* overrides
import type { CSSProperties } from "react";
import { BarChart } from "insightcharts";
const data = [
{ browser: "Chrome", share: 64 },
{ browser: "Safari", share: 19 },
{ browser: "Edge", share: 5 },
{ browser: "Firefox", share: 3 },
];
// Any ancestor can set --ic-* variables; the chart resolves them against its own DOM node.
const brand = { "--ic-accent": "#0f766e", "--ic-grid": "color-mix(in oklch, #0f766e 18%, transparent)", "--ic-font-display": "Georgia, serif" } as CSSProperties;
export default function CssVars() {
return (
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", gap: 28 }}>
<BarChart data={data} x="browser" y="share" format="{value}%" orientation="horizontal" height={190} title="Default tokens" />
<div style={brand}>
<BarChart data={data} x="browser" y="share" format="{value}%" orientation="horizontal" height={190} title="Scoped --ic-* overrides" />
</div>
</div>
);
}
| Variable | Token | Used for |
|---|---|---|
--ic-surface | surface | What sits behind the marks; the gaps and rings that separate them. |
--ic-ink | ink | Titles and primary values. |
--ic-ink-secondary | inkSecondary | Subtitles, legends, direct labels. |
--ic-muted | muted | Axis labels and source lines. |
--ic-grid | grid | Hairline gridlines. |
--ic-axis | axis | Baselines and axis lines. |
--ic-accent | accent | The highlighted series, and a single series. |
--ic-context | context | De-emphasised series behind a highlight. |
--ic-positive, --ic-negative | positive, negative | Up and down. |
--ic-series-1 to --ic-series-8 | categorical | Identity colours, in order. |
--ic-font | fontFamily | Chart and chrome text. |
--ic-font-display | displayFamily | Titles and big numbers. |
:root {
--ic-accent: #0f766e;
--ic-font-display: "Fraunces", Georgia, serif;
}
.dark {
--ic-accent: #2dd4bf;
}The sequential and diverging ramps, and the numeric tokens fontSize, radius and lineWidth, have no variables. Set them with a theme object.
Theme objects and createTheme
The theme prop on any chart, or on InsightProvider, accepts a full theme or a partial one. Tokens at the top level apply to both modes; light and dark override one mode. A partial theme is merged over the surrounding theme, so providers and charts only state what differs.
import { BarChart, InsightProvider } from "insightcharts";
<InsightProvider theme={{ accent: "#7c3aed", radius: 2, dark: { accent: "#a78bfa" } }}>
<BarChart theme={{ lineWidth: 3 }} ... />
</InsightProvider>createTheme(input, base?) does the same merge ahead of time and returns a complete theme you can export and reuse. base defaults to the default theme.
import { createTheme, themes } from "insightcharts/themes";
export const brand = createTheme({
name: "brand",
accent: "var(--brand, #7c3aed)",
categorical: ["#7c3aed", "#0891b2", "#ca8a04", "#be123c"],
light: { grid: "#ebe9f5" },
dark: { grid: "#2a2740" },
});
// Or start from a preset.
export const compact = createTheme({ fontSize: 12, lineWidth: 1.5 }, themes.shadcn);Tokens
| Token | Type | Purpose |
|---|---|---|
surface, ink, inkSecondary, muted, grid, axis | colour | Chrome, from background to baselines. |
accent, context | colour | The highlighted series, and the grey used for every other series. |
positive, negative | colour | Direction. Always paired with a non-colour cue. |
categorical | colour[] | Identity colours, assigned to entities in fixed order. |
sequential | colour[] | One hue, light to dark, for magnitude. |
diverging | colour[] | Negative pole, neutral midpoint, positive pole. |
fontFamily, displayFamily | string | Fonts for body text and for titles. |
fontSize | number | Base chart text size in pixels. Default 12. |
radius | number | Corner radius of bar ends in pixels. Default 4. |
lineWidth | number | Line thickness in pixels. Default 2. |
Presets
themes from insightcharts/themes is a record of every theme by name. It holds the eight presets in this table and the eight poster themes. Each preset is also a named export (editorialTheme, ledgerTheme, monoTheme, earthTheme, scienceTheme, posterTheme, shadcnTheme). This table is drawn from the record itself; the preview toolbar on every component page lists the same presets.
| Preset | Light, on its surface | Dark, on its surface | Designed for |
|---|---|---|---|
| themes.insight | the page | the page | The default theme. It reads shadcn/ui variables for surface and ink. |
| themes.shadcn | the page | the page | Reads the page variables and takes series colours from --chart-1 to --chart-5. |
| themes.editorial | #f8f7f4 | #15171a | Newspaper graphics. Off-white surface, blue-black ink, serif titles, carmine accent. |
| themes.ledger | #fff1e5 | #201c1a | Financial reports. Salmon surface, dark blue accent, claret and teal series. |
| themes.mono | #ffffff | #0b0b0c | High contrast. Black and white text and axes, square marks, a thick baseline. |
| themes.earth | #f4f1ea | #1b1a17 | Environmental reports. Warm grey surface, green accent, blue, ochre and clay series. |
| themes.science | #f6f8fa | #0e151c | Journal figures. Blue-grey surface, thin lines, small corner radius, teal accent. |
| themes.poster | #f6f3fb | #16112e | Posters and social cards. Saturated magenta accent, thick lines, round bar ends. |
Presets and their surfaces
insight and shadcn follow the page they are on. The other six are designed for a specific background colour in each mode, and its separators, marker rings and sticky table headers are drawn in that colour. On any other background they show as off-colour seams, and the contrast the palette was checked for no longer holds.
themeSurfaces[name].light and .dark are those backgrounds. Paint the section, card or page the charts sit in, and force the mode to match it:
Buses carried 3.6bn trips
The air fare index rose from 97 in 2021 to 131 in 2024
Fare index, 2019 = 100
themeSurfaces.editorial.light = #f8f7f4import { useState } from "react";
import { BarChart, BigNumber, InsightProvider, LineChart } from "insightcharts";
import { themes, themeSurfaces } from "insightcharts/themes";
const fares = [
{ year: "2019", Rail: 100, Bus: 100, Air: 100 },
{ year: "2020", Rail: 103, Bus: 102, Air: 91 },
{ year: "2021", Rail: 106, Bus: 105, Air: 97 },
{ year: "2022", Rail: 110, Bus: 111, Air: 118 },
{ year: "2023", Rail: 116, Bus: 119, Air: 127 },
{ year: "2024", Rail: 121, Bus: 124, Air: 131 },
];
const trips = [
{ mode: "Rail", trips: 1.7 },
{ mode: "Bus", trips: 3.6 },
{ mode: "Air", trips: 0.3 },
];
// The presets that paint for a surface of their own. `insight` and `shadcn` follow the page.
const presets = Object.keys(themeSurfaces) as (keyof typeof themeSurfaces)[];
export default function ThemeSurfaces() {
const [preset, setPreset] = useState<keyof typeof themeSurfaces>("editorial");
const [mode, setMode] = useState<"light" | "dark">("light");
const chip = { padding: "4px 12px", borderRadius: 999, border: "1px solid var(--border)", fontSize: 13 };
return (
<div style={{ display: "grid", gap: 16 }}>
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" }}>
{presets.map((name) => (
<button key={name} type="button" aria-pressed={preset === name} onClick={() => setPreset(name)} style={{ ...chip, background: preset === name ? "var(--accent)" : "transparent" }}>
{name}
</button>
))}
<span style={{ width: 1, height: 20, background: "var(--border)", margin: "0 4px" }} />
{(["light", "dark"] as const).map((m) => (
<button key={m} type="button" aria-pressed={mode === m} onClick={() => setMode(m)} style={{ ...chip, background: mode === m ? "var(--accent)" : "transparent" }}>
{m}
</button>
))}
</div>
{/* The section is painted with the surface the preset was designed for, and the mode is
forced to match it, so the charts do not depend on the page around them. */}
<section style={{ background: themeSurfaces[preset][mode], borderRadius: 12, padding: 24 }}>
<InsightProvider theme={themes[preset]} mode={mode}>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(240px, 1fr))", gap: 28, alignItems: "start" }}>
<div style={{ display: "grid", gap: 20 }}>
<BigNumber value={131} format="integer" label="Air fares, index (2019 = 100)" delta={0.031} deltaLabel="vs 2023" upIsGood={false} />
<BarChart data={trips} x="mode" y="trips" format="{value}bn" height={170} title="Buses carried 3.6bn trips" />
</div>
<LineChart data={fares} x="year" y={["Rail", "Bus", "Air"]} height={300} title="The air fare index rose from 97 in 2021 to 131 in 2024" subtitle="Fare index, 2019 = 100" note="Illustrative data" />
</div>
</InsightProvider>
</section>
<code style={{ fontSize: 12, opacity: 0.7 }}>
themeSurfaces.{preset}.{mode} = {themeSurfaces[preset][mode]}
</code>
</div>
);
}
import { InsightProvider } from "insightcharts";
import { ledgerTheme, themeSurfaces } from "insightcharts/themes";
<section style={{ background: themeSurfaces.ledger.light }}>
<InsightProvider theme={ledgerTheme} mode="light">{children}</InsightProvider>
</section>To follow the page's mode instead of forcing one, read it with useColorMode and pick the matching surface, as the first demo on this page does.
The shadcn preset
The shadcn preset reads more of the page's variables than the default theme does. Series colours come from --chart-1 to --chart-5 and the accent from --chart-1. Use it when charts must match an existing shadcn palette. shadcn's stock chart colours are not ordered for colour-vision-deficiency separation, so keep direct labels or legends on.
import { InsightProvider } from "insightcharts";
import { themes } from "insightcharts/themes";
<InsightProvider theme={themes.shadcn}>{children}</InsightProvider>Poster themes
A poster theme is a theme for the infographics and the poster frame. It has a strong painted ground, type colours for that ground and four or five mark colours. posterThemes from insightcharts/themes holds the eight by name, and the same eight are in the themes record. Each is also a named export: noirTheme, magentaTheme, duskTheme, paperTheme, oceanTheme, forestTheme, blueprintTheme and sunriseTheme.
Near-black sheet, white type, yellow and sky blue marks.
#0d0d0f
Saturated magenta sheet, pale pink blocks, cyan accent.
#c2006b
Violet gradient, marks from orange to yellow.
linear-gradient(160deg, #24104f 0%, #3d1663 55%, #5e2067 100%)
Warm off-white stock, black ink, one red.
#f3ecdf
dark: #1d1b18
Navy to teal gradient, aqua and coral marks.
linear-gradient(180deg, #041c3a 0%, #06365a 55%, #084c5c 100%)
Dark green sheet, lime and cream marks.
#0f3d2a
Cobalt sheet, white linework, yellow accent.
#1447c8
Cream to peach gradient, plum and tangerine marks.
linear-gradient(180deg, #fff1e0 0%, #ffdfc6 100%)
A poster theme differs from a preset in three ways.
- One look.
noir,magenta,dusk,ocean,forest,blueprintandsunrisehave the same tokens inlightanddark. The poster has the same colours on a light page and on a dark page.paperhas a second variant: itsdarktokens are the same design on charcoal stock. - A painted ground.
posterBackgrounds[name].lightand.darkare the CSS background of each theme.dusk,oceanandsunriseare gradients, so the value is a CSSbackgroundand not a single colour. The theme'ssurfacetoken is the solid colour closest to the ground. Charts draw the gaps between touching marks insurface. - A small palette. A poster theme has four or five categorical colours, not eight. Fold further entities into "Other" or shade by value with the sequential ramp.
The poster frame and every infographic template take the name as theme and paint the ground themselves:
import { Poster } from "insightcharts";
<Poster theme="ocean" title="..." panels={panels} />To use a poster theme without the frame, paint the ground on a container and force one mode inside it:
import { InsightProvider } from "insightcharts";
import { posterBackgrounds, posterThemes } from "insightcharts/themes";
<section style={{ background: posterBackgrounds.ocean.light }}>
<InsightProvider theme={posterThemes.ocean} mode="light">{children}</InsightProvider>
</section>The text colours of each theme have a contrast of at least 4.5:1 against every colour stop of its ground. The categorical colours and the accent have at least 3:1. The display faces are system font stacks. Set --ic-font-display on an ancestor to use your own face.
Restyling the chrome
Titles, legends and source lines are real HTML. Each element carries a data-slot attribute (ic-figure, ic-title, ic-subtitle, ic-legend, ic-plot, ic-source, ic-table, ic-actions), so you can restyle them from your own CSS.
[data-slot="ic-title"] {
font-size: 1.25rem;
letter-spacing: -0.02em;
}