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.
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 chrome // 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 ... /> {/* Now uses 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.
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