ThemeProvider applies consistent colors, typography, tooltips, and borders to every Semiotic chart it wraps. It ships with17 named presets covering light/dark variants of six distinct design vocabularies, plus a high-contrast accessibility preset. You can also pass a custom theme object.
For stable category-to-color assignments across several charts, pair a theme with CategoryColorProvider from semiotic. It intentionally is not re-exported from semiotic/themes/react: that narrow entry contains only theme state, while category mapping also brings the chart palette resolver.
Quick Start
JSX
import { ThemeProvider, BarChart } from "semiotic" // Named preset — one line for brand-consistent charts <ThemeProvider theme="tufte"> <BarChart data={data} categoryAccessor="name" valueAccessor="value" /> </ThemeProvider> // Custom theme object — only specify what you want to override <ThemeProvider theme={{ colors: { primary: "#e63946", categorical: ["#e63946", "#457b9d", "#a8dadc", "#1d3557"], background: "#f1faee", text: "#1d3557", }, typography: { fontFamily: '"Inter", sans-serif' }, }}> <BarChart data={data} categoryAccessor="name" valueAccessor="value" /> </ThemeProvider>
15 Named Presets
Select a preset to see it applied to four chart types simultaneously. Each preset defines colors, typography, tooltips, and border radius:
Base
Tufte
Pastels
BI Tool
Italian
Journalist
Playful
Carbon
JSX
// All 15 preset names: // "light" "dark" "high-contrast" // "tufte" "tufte-dark" // "pastels" "pastels-dark" // "bi-tool" "bi-tool-dark" // "italian" "italian-dark" // "journalist" "journalist-dark" // "playful" "playful-dark" // "carbon" "carbon-dark" // Import theme objects directly for inspection or extension import { TUFTE_LIGHT, TUFTE_DARK, PASTELS_LIGHT } from "semiotic/themes" <ThemeProvider theme={{ ...TUFTE_LIGHT, colors: { ...TUFTE_LIGHT.colors, primary: "#cc0000" }, }}> {/* Tufte with a red accent */} </ThemeProvider>
Dark / Light Mode Integration
ThemeProvider is reactive — changing the themeprop re-initializes the store, so you can wire it to your app's dark mode toggle, a media query, or a context value.
Reactive mode switching
Pass a string preset or object — ThemeProvider re-applies whenever the prop changes:
JSX
import { ThemeProvider, BarChart } from "semiotic" function Dashboard({ children }) { const [dark, setDark] = useState( window.matchMedia("(prefers-color-scheme: dark)").matches ) return ( <ThemeProvider theme={dark ? "dark" : "light"}> <button onClick={() => setDark(d => !d)}>Toggle mode</button> {children} </ThemeProvider> ) }
Preserving custom colors across mode changes
Passing a string preset like "dark" replaces the entire theme — including categorical colors. To keep your brand palette in both modes, pass an object with mode set. This merges your overrides onto the matching base theme (light or dark):
JSX
const MY_COLORS = ["#e63946", "#457b9d", "#a8dadc", "#1d3557"] function Dashboard({ children }) { const [dark, setDark] = useState(false) // Object with `mode` → merges onto DARK_THEME or LIGHT_THEME base, // so background/text/grid adapt to mode while categorical stays yours. const theme = { mode: dark ? "dark" : "light", colors: { categorical: MY_COLORS }, } return ( <ThemeProvider theme={theme}> {children} </ThemeProvider> ) } // Equivalent explicit approach (if you need full control): import { LIGHT_THEME, DARK_THEME } from "semiotic" const lightTheme = { ...LIGHT_THEME, colors: { ...LIGHT_THEME.colors, categorical: MY_COLORS } } const darkTheme = { ...DARK_THEME, colors: { ...DARK_THEME.colors, categorical: MY_COLORS } } <ThemeProvider theme={dark ? darkTheme : lightTheme}>
Merge rules: String preset (e.g. "dark") → full replacement. Object with mode → merge onto the matching base theme. Object without mode → merge onto the current theme (partial override, keeps existing mode/colors).
CSS custom properties interop
ThemeProvider sets --semiotic-* CSS vars on a wrapper<div>. If your host app also sets--semiotic-* vars (e.g. on :root), ThemeProvider's closer div wins for charts inside it. To let your app's tokens flow through, use CSS-only theming instead — skip ThemeProvider and set the vars yourself:
CSS
/* Let your app's design tokens drive chart colors directly */ :root { --semiotic-bg: var(--app-surface); --semiotic-text: var(--app-text); --semiotic-text-secondary: var(--app-text-muted); --semiotic-grid: var(--app-border-subtle); --semiotic-border: var(--app-border); --semiotic-primary: var(--app-accent); --semiotic-font-family: var(--app-font); } /* Dark mode automatically inherits when your app switches tokens */ [data-theme="dark"] { --app-surface: #1a1a2e; --app-text: #e0e0e0; /* ... Semiotic vars follow because they reference --app-* */ } /* Or combine: use ThemeProvider for categorical/sequential colors, and let CSS vars handle the rest */
JSX
// Hybrid approach: ThemeProvider for palette only, CSS vars for decoration // Since this object has no `mode`, it merges onto the current theme // without overriding background/text/grid (which come from your CSS vars) <ThemeProvider theme={{ colors: { categorical: BRAND_COLORS, sequential: "viridis", }, }}> <BarChart data={data} categoryAccessor="name" valueAccessor="value" /> </ThemeProvider>
CSS Custom Properties Reference
ThemeProvider sets CSS custom properties on a wrapperdiv, so surrounding UI can inherit the active theme. It also sets a data-semiotic-theme attribute when using a named preset, enabling CSS-only theme switching for SSR:
CSS
/* ThemeProvider emits all of these: */ --semiotic-bg /* colors.background */ --semiotic-text /* colors.text */ --semiotic-text-secondary /* colors.textSecondary */ --semiotic-grid /* colors.grid */ --semiotic-border /* colors.border */ --semiotic-primary /* colors.primary */ --semiotic-focus /* colors.focus */ --semiotic-font-family /* typography.fontFamily */ --semiotic-border-radius /* borderRadius */ --semiotic-tooltip-bg /* tooltip.background */ --semiotic-tooltip-text /* tooltip.text */ --semiotic-tooltip-radius /* tooltip.borderRadius */ --semiotic-tooltip-font-size /* tooltip.fontSize */ --semiotic-tooltip-shadow /* tooltip.shadow */ --semiotic-selection-color /* colors.selection */ --semiotic-selection-opacity /* colors.selectionOpacity */ --semiotic-diverging /* colors.diverging */
CSS
/* Style surrounding UI to match the chart theme */ .dashboard-panel { background: var(--semiotic-bg); color: var(--semiotic-text); font-family: var(--semiotic-font-family); border: 1px solid var(--semiotic-border); border-radius: var(--semiotic-border-radius, 8px); } /* CSS-only theme switching without JS (SSR-friendly) */ [data-semiotic-theme="tufte"] .dashboard-panel { font-family: Georgia, serif; } /* Or use themeToCSS() to generate all vars at build time */
useTheme Hook
Custom components can read the active theme with useTheme(), which returns the full SemioticTheme object:
JSX
import { useTheme } from "semiotic" function DashboardHeader({ title, subtitle }) { const theme = useTheme() return ( <div style={{ background: theme.colors.background, borderBottom: `1px solid ${theme.colors.border}`, padding: "16px 24px", fontFamily: theme.typography.fontFamily, }}> <h2 style={{ color: theme.colors.text, fontSize: theme.typography.titleSize, margin: 0 }}> {title} </h2> {subtitle && ( <p style={{ color: theme.colors.textSecondary, fontSize: theme.typography.labelSize, margin: "4px 0 0" }}> {subtitle} </p> )} </div> ) }
Theme Serialization
The semiotic/themes entry point exports utilities for converting themes to CSS or design tokens:
JSX
import { themeToCSS, themeToTokens, resolveThemePreset, THEME_PRESETS, } from "semiotic/themes" // Generate a CSS stylesheet for a theme const css = themeToCSS(resolveThemePreset("tufte"), ".my-charts") // .my-charts { // --semiotic-bg: #fffff8; // --semiotic-text: #111111; // ... // } // Generate design tokens (DTCG format, Style Dictionary compatible) const tokens = themeToTokens(resolveThemePreset("tufte")) // { semiotic: { bg: { $value: "#fffff8", $type: "color" }, ... } } // Generate CSS for SSR with data-semiotic-theme attribute const ssrCSS = Object.entries(THEME_PRESETS) .map(([name, theme]) => themeToCSS(theme, `[data-semiotic-theme="${name}"]`) ) .join("\n\n")
Importing a brand's design tokens
The inverse of themeToTokens reads aW3C Design Tokens (DTCG)file into a Semiotic theme — so charts theme from the same source of truth as the rest of the product. It round-trips exactly for tokens Semiotic emitted (the semiotic.* group), and falls back to leaf-name heuristics for a foreign brand file (color.brand.primary,semantic.error, fg.default, …), resolving{alias} references and detecting light/dark from the background. Pin anything unconventional with the mapping option.
JSX
import { designTokensToTheme } from "semiotic/themes" import { ThemeProvider } from "semiotic" import brandTokens from "./design-tokens.json" // your DTCG file const theme = designTokensToTheme(brandTokens, { // optional: pin a role to a token path when names are unconventional mapping: { categorical: "color.chart.qualitative" }, }) <ThemeProvider theme={theme}><Dashboard /></ThemeProvider> // Round-trips with the exporter: // designTokensToTheme(themeToTokens(t)) recovers t's roles + palette.
CSS-Only Theming
You don't need ThemeProvider at all — Semiotic charts read --semiotic-* CSS custom properties from any ancestor element. Set them in your stylesheet or inline:
CSS
/* In your global CSS */ .brand-charts { --semiotic-bg: #1a1a2e; --semiotic-text: #ededed; --semiotic-text-secondary: #aaa; --semiotic-grid: #333; --semiotic-border: #555; --semiotic-font-family: Georgia, serif; --semiotic-tooltip-bg: #1a1a2e; --semiotic-tooltip-text: #ededed; --semiotic-tooltip-radius: 8px; } /* In JSX — no ThemeProvider needed */ <div className="brand-charts"> <BarChart data={data} categoryAccessor="name" valueAccessor="value" /> <LineChart data={lines} xAccessor="x" yAccessor="y" /> </div>
Accessibility
The "high-contrast" preset uses the Wong 2011 color-blind-safe palette (COLOR_BLIND_SAFE_CATEGORICAL), bold text, and high-contrast borders. Import the palette directly for custom themes:
JSX
import { COLOR_BLIND_SAFE_CATEGORICAL } from "semiotic" // Use the 8-color CB-safe palette in a custom theme <ThemeProvider theme={{ colors: { categorical: COLOR_BLIND_SAFE_CATEGORICAL, text: "#000000", border: "#000000", }, }}> <BarChart data={data} categoryAccessor="group" valueAccessor="value" colorBy="group" /> </ThemeProvider> // Or just use the high-contrast preset <ThemeProvider theme="high-contrast"> {/* charts */} </ThemeProvider>
The diagnoseConfig utility also checks contrast ratios — run npx semiotic-ai --doctor to audit your chart configs.
IBM Carbon Palettes
The "carbon" / "carbon-dark" presets use IBM's Carbon Design System color language. In addition to the 4-color categorical palette built into the theme, Semiotic exports the full 14-color categorical palette and alert colors for specialized use. The Carbon diverging palettes are available via the theme (e.g.,colors.diverging).
Full 14-color categorical palette
Use CARBON_CATEGORICAL_14 when you need more than 4 categories:
#6929c4
#1192e8
#005d5d
#9f1853
#fa4d56
#570408
#198038
#002d9c
#ee538b
#b28600
#009d9a
#012749
#8a3800
#a56eff
JSX
import { CARBON_CATEGORICAL_14 } from "semiotic" <ThemeProvider theme="carbon"> <Scatterplot data={data} colorBy="category" colorScheme={CARBON_CATEGORICAL_14} /> </ThemeProvider>
Sequential and diverging schemes
The Carbon theme sets sequential: "blues" anddiverging: "RdBu". Heatmaps automatically use the sequential scheme:
JSX
// Sequential — set via theme or colorScheme prop <ThemeProvider theme="carbon"> <Heatmap colorScheme="blues" ... /> </ThemeProvider> // Diverging — use the theme's diverging field <ThemeProvider theme={{ ...CARBON_LIGHT, colors: { ...CARBON_LIGHT.colors, diverging: "RdBu" }, }}> <Heatmap ... /> {/* Uses the diverging red-blue scale */} </ThemeProvider>
Alert palette
CARBON_ALERT provides danger, warning, success, and info colors for status indicators and threshold annotations:
JSX
import { CARBON_ALERT } from "semiotic" <LineChart data={data} xAccessor="time" yAccessor="latency" annotations={[ { type: "y-threshold", value: 200, label: "Warning", color: CARBON_ALERT.warning }, { type: "y-threshold", value: 500, label: "Danger", color: CARBON_ALERT.danger }, { type: "band", y0: 0, y1: 100, label: "Healthy", fill: CARBON_ALERT.success }, ]} />
Serialization & custom themes
A theme is a plain object — start from a preset and override, or build your own. resolveThemePreset(name) returns a preset's theme object so you can spread and tweak it:
JSX
import { ThemeProvider } from "semiotic" import { resolveThemePreset } from "semiotic" const tufte = resolveThemePreset("tufte") const myTheme = { ...tufte, mode: "light", colors: { ...tufte.colors, categorical: ["#1f77b4", "#ff7f0e", "#2ca02c"] }, } <ThemeProvider theme={myTheme}>{/* charts */}</ThemeProvider>
For design-system pipelines, two serializers turn a theme into portable artifacts:
themeToCSS(theme, selector = ":root") — emits the theme as a CSS custom-property block scoped to selector. Drop it in a stylesheet and the charts (which read --semiotic-* vars) pick it up with no ThemeProvider needed.themeToTokens(theme) — emits a flat token object (DTCG- friendly) for handing colors and typography to a token store or other tools.
JSX
import { themeToCSS, themeToTokens, resolveThemePreset } from "semiotic" const css = themeToCSS(resolveThemePreset("carbon"), ".carbon-charts") // → ".carbon-charts { --semiotic-bg: …; --semiotic-primary: …; … }" const tokens = themeToTokens(resolveThemePreset("carbon")) // → { "--semiotic-bg": "…", … } — feed a design-token store
Because the runtime reads CSS variables, a serialized theme and aThemeProvider theme are interchangeable — emit CSS at build time for zero-runtime theming, or provide the object at runtime for dynamic switching.
Preserve an authored light design
When adding dark mode to an existing custom chart, keep its original light colors as CSS-variable fallbacks and define only the dark overrides. This preserves the light rendering exactly while giving SVG, canvas, controls, and native form chrome one semantic mode boundary. The resolved data-semiotic-theme-mode attribute also works for custom theme objects; it is not coupled to a preset name.
JSX
/* Original light values stay at the use site. */ const paper = "var(--essay-chart-paper, #fffefa)" const ink = "var(--essay-chart-ink, #203b32)" /* Add only the alternate mode. */ [data-semiotic-theme-mode="dark"] .essay { --essay-chart-paper: #161616; --essay-chart-ink: #f4f4f4; color-scheme: dark; } <rect fill={paper} stroke={ink} />
Test the complete light → dark → light round trip. Exact restoration is important for settled canvases, whose retained scene may otherwise keep stale resolved CSS paint after the DOM has changed.
Props Reference
ThemeProvider
| Prop | Type | Required | Default | Description |
|---|
theme | ThemePresetName | Partial<SemioticTheme> | — | "light" | A named preset string or a partial theme object. Named presets: "light", "dark", "high-contrast", "tufte", "tufte-dark", "pastels", "pastels-dark", "bi-tool", "bi-tool-dark", "italian", "italian-dark", "journalist", "journalist-dark", "playful", "playful-dark", "carbon", "carbon-dark". Partial objects are deep-merged with the active theme. |
children | ReactNode | Yes | — | Chart components to be themed. |
SemioticTheme Shape
| Prop | Type | Required | Default | Description |
|---|
mode | "light" | "dark" | "auto" | — | "light" | Theme mode label. Used by some components for mode-aware defaults. |
colors.primary | string | — | "#00a2ce" | Default chart accent color. Maps to --semiotic-primary. |
colors.categorical | string[] | — | d3 category10 | Categorical palette. Each chart's colorBy draws from this. |
colors.sequential | string | — | "blues" | d3-scale-chromatic sequential scheme name (e.g. "blues", "viridis"). |
colors.diverging | string | — | undefined | d3-scale-chromatic diverging scheme name (e.g. "RdBu"). Maps to --semiotic-diverging. |
colors.background | string | — | "transparent" | Chart background. Maps to --semiotic-bg. |
colors.text | string | — | "#333" | Primary text color (titles, axis labels). Maps to --semiotic-text. |
colors.textSecondary | string | — | "#666" | Secondary text color (tick labels). Maps to --semiotic-text-secondary. |
colors.grid | string | — | "#e0e0e0" | Grid line color. Maps to --semiotic-grid. |
colors.border | string | — | "#ccc" | Axis / card border color. Maps to --semiotic-border. |
colors.focus | string | — | "#005fcc" | Focus ring color for keyboard navigation. Maps to --semiotic-focus. |
colors.selection | string | — | undefined | Linked hover/brush highlight color. Maps to --semiotic-selection-color. |
colors.selectionOpacity | number | — | 0.2 | Non-selected element opacity (0-1). Maps to --semiotic-selection-opacity. |
typography.fontFamily | string | — | "sans-serif" | Font family for all chart text. Maps to --semiotic-font-family. |
typography.titleSize | number | — | 16 | Font size (px) for chart titles. |
typography.labelSize | number | — | 12 | Font size (px) for axis labels. |
typography.tickSize | number | — | 10 | Font size (px) for tick labels. |
tooltip.background | string | — | "rgba(0,0,0,0.85)" | Tooltip background. Maps to --semiotic-tooltip-bg. |
tooltip.text | string | — | "white" | Tooltip text color. Maps to --semiotic-tooltip-text. |
tooltip.borderRadius | string | — | "6px" | Tooltip corner radius. Maps to --semiotic-tooltip-radius. |
tooltip.shadow | string | — | "0 2px 8px rgba(0,0,0,0.15)" | Tooltip box shadow. Maps to --semiotic-tooltip-shadow. |
borderRadius | string | — | "8px" | Global border radius token. Maps to --semiotic-border-radius. |
- Styling — per-mark styling, data-driven colors, SVG patterns
- Theme Explorer — interactive playground to customize and export themes
- Legends — legends automatically respect theme colors and interactive state
- Linked Charts — coordinated views that share a theme
- Benchmark Dashboard— recipe with live theme switching across 4 views