Skip to content

Theming ​

Chart canvas colors come from a ChartTheme object. With the UI shell, that object is read from CSS variables, so one stylesheet colors both the chrome and the canvas.

To try colors without writing code, use the theme builder: it edits every token below in a dark and a light shell, checks text contrast, and exports a theme object and a CSS block.

Built-in themes ​

ExportLook
darkThemeDefault dark
lightThemeLight
neutralThemeNeutral graphite
highContrastDarkThemeBlack canvas, white text, stronger lines (see Accessibility presets)
highContrastLightThemeWhite canvas, black text, stronger lines
ts
import { createChart, lightTheme } from '@chart-on/core';

const chart = createChart(el, { theme: lightTheme });

With the UI shell, use theme: 'light' or shell.setTheme('light') (see Light, dark and system).

Default palette ​

The dark and light themes share one palette: neutral greys with a faint violet tint, a violet accent, and mint and coral for rising and falling bars.

RoleDarkLight
Canvas#121217#ffffff
Text#babac6#15151b
Muted text#8b8b99#696977
Accent, line series, drawings#8b75ff#5f43e6
Up#3cc4a0#0f8268
Down#e8574a#c9412f

Text, muted text, the accent and the up and down colors all reach at least 4.5:1 contrast on the canvas (WCAG AA). Up and down also differ in lightness and in their blue component, so they stay apart for red-green color blindness. The light theme uses darker up and down shades to keep that contrast on white.

Heatmap ramp ​

The heatmap color ramp (heatmap, --pc-heatmap) runs from low to high intensity:

ThemeLow → high
Dark#211e36, #493088, #6656d7, #2ea4dc, #48dcc1, #fced95
Light#f3f2fe, #abe6e9, #51cbbd, #2c90d7, #5f3ed8, #541b81

The lowest stop is a faint violet tint of the canvas, so sparse cells fade into the background instead of showing as a colored block. From there each stop moves further from the canvas in lightness: brighter on the dark theme, darker on the light one. Intensity therefore still reads in greyscale and with color blindness. The hues pass through the accent and the mint of the up color, and avoid the coral of the down color, so the ramp doesn't read as falling prices. neutralTheme keeps its original ramp.

If you supply your own ramp, follow the same rules: start close to your canvas color and change lightness in one direction.

Chart colors ​

Pass any subset of ChartTheme. Missing keys fall back to darkTheme; applyOptions({ theme }) changes only the keys it is given.

ts
import { createChart, type Bar } from '@chart-on/core';
import type { ExampleContext } from './types';
import { sampleBars } from './sample-data';

export default function mount(el: HTMLElement, { theme }: ExampleContext) {
  // Keys left out fall back to `darkTheme`, so spread a full base theme first:
  // here the page's `theme`, in an app `darkTheme` or `lightTheme`. Blue and
  // orange stay apart for red-green color blindness.
  const chart = createChart(el, {
    theme: { ...theme, up: '#3d8bd9', down: '#e07b39' },
  });
  const bars = sampleBars();
  chart.addCandles({ title: 'SAMPLE' }).setData(bars);
  // No `color`: the line takes the theme's `line` color.
  chart.addLine({ title: 'SMA 20' }).setData(sma(bars, 20));
  chart.fitContent();

  // Change colors later: only the keys you pass change, and every series
  // that uses them redraws.
  chart.applyOptions({ theme: { line: '#a35fd0' } });

  return () => chart.destroy();
}

/** Simple moving average of the closes over `length` bars. */
function sma(bars: Bar[], length: number) {
  const out = [];
  let sum = 0;
  for (let i = 0; i < bars.length; i++) {
    sum += bars[i]!.close;
    if (i >= length) sum -= bars[i - length]!.close;
    if (i >= length - 1) out.push({ time: bars[i]!.time, value: sum / length });
  }
  return out;
}
KeyCSS tokenUsed for
background--pc-chart-bgChart background
backgroundBottom--pc-chart-bg-bottomBottom of a vertical background gradient from background; optional, solid background when unset. Screenshots include it
grid--pc-chart-gridGrid lines
gridHorizontal, gridVertical--pc-chart-grid-h, --pc-chart-grid-vPer-direction grid colors; optional, default to grid
separator--pc-chart-separatorPane separators
text--pc-chart-textAxis labels, legend
textMuted--pc-chart-text-mutedSecondary axis and legend text
fontFamily--pc-chart-fontFont of all chart text, unless typography overrides it
fontSize--pc-chart-font-sizeSize of chart text in px, unless typography overrides it
typography--pc-chart-font-weight, --pc-chart-numeralsWeight, numerals and per-surface fonts; see Typography
crosshair--pc-chart-crosshairCrosshair lines
labelBackground--pc-chart-label-bgCrosshair and last-value labels
labelText--pc-chart-label-textText on those labels
up, down--pc-up, --pc-downCandle colors
upWick, downWick--pc-up-wick, --pc-down-wickCandle wicks; optional, default to up / down
upBorder, downBorder--pc-up-border, --pc-down-borderCandle body outlines; optional, none when unset
line--pc-line-seriesDefault line and histogram color
drawing--pc-drawingDrawing tools
drawingSelected--pc-drawing-selectedDefault color of text drawings
drawingFill--pc-drawing-fillDrawing fills
referenceLine--pc-chart-reference-linePrevious close, visible high, low and average reference lines
bid, ask--pc-bid, --pc-askBid and ask lines
depthBid, depthAsk--pc-depth-bid, --pc-depth-askDepth lane bars; optional, default to bid and ask
extendedHours--pc-extended-hoursPre and post session price label
alert--pc-alertAlert lines
timeMark--pc-time-markBadges of the data source's time-axis markers; optional, defaults to line
annotationFill--pc-annotation-fillDefault fill of time bands and price zones; keep it translucent
heatmap--pc-heatmapHeatmap color ramp, low to high, as a comma-separated list
watermark--pc-chart-watermarkWatermark text; keep it faint with a low alpha
levelNeutral, levelRed, levelOrange, levelGreen, levelBlue, levelPurple--pc-level-neutral, --pc-level-red, --pc-level-orange, --pc-level-green, --pc-level-blue, --pc-level-purpleNamed level colors a level can pick ({ value: 0.5, color: 'levelBlue' }), kept so saved levels that name one keep their color. The default levels use the shared level rule instead. Optional: a level falls back to the drawing color when its token is unset, and without levelNeutral the rule's levels do too

Colors accept hex (#rgb, #rrggbb, #rrggbbaa), rgb()/rgba() and named CSS colors. The same formats work in the CSS tokens. chartThemeTokens maps each key to its token in code.

Opacity is honored everywhere a color is used: candle bodies, wicks and borders, lines, areas and histograms on the GPU layer, and grid, crosshair, labels and drawings on the canvas layers. A translucent background lets the page show through the chart.

A candle series' own color options (upColor, upWickColor, upBorderColor and the down ones) win over the theme for that series.

Typography ​

fontFamily and fontSize set one font for all chart text. For a branded embed, the optional typography key adds a weight and a numeral style, and lets each text surface use its own font:

ts
chart.applyOptions({
  theme: {
    typography: {
      fontFamily: '"Brand Sans", system-ui, sans-serif',
      fontWeight: 450,
      numerals: 'tabular',
      axis: { fontFamily: '"Brand Mono", ui-monospace, monospace' },
      legend: { fontSize: 14, fontWeight: 500 },
      watermark: { fontWeight: 800 },
    },
  },
});

The baseline (fontFamily, fontSize, fontWeight, numerals) applies to every surface. Each surface block takes the same four fields, and a field it leaves out falls through to the baseline. A baseline field left out falls through to the theme's fontFamily and fontSize, a regular weight (400) and tabular numerals.

SurfaceText
axisPrice and time axis ticks, and the price, crosshair and countdown labels on the axes
legendLegend rows and the heatmap inspector
labelsLabels inside the plot: price lines, alerts, orders, trade badges, tooltips and indicator messages
drawingsDrawing labels, and the default font of text drawings
watermarkWatermark family and weight; sizes come from the watermark options
markersText of bar markers (chart.annotations.setMarkers)

Some surfaces differ from the baseline by default, to keep the look they always had. The legend is 1 px larger than the baseline size unless its block sets fontSize, and the watermark is weight 600 unless its block sets fontWeight. Drawing styles (fontSize, bold, italic) still win over the theme for a single drawing.

Typography merges per surface: applyOptions({ theme: { typography: { axis: { fontSize: 13 } } } }) keeps the other blocks. resolveFont(theme, surface) returns the font a surface draws with, and fontCss(font) turns it into a CSS font string, which is useful for custom series and drawing tools.

Numerals ​

numerals: 'tabular' (the default) gives every digit the same width, so a live price does not shift sideways as it ticks. 'proportional' uses the font's natural digit widths. The DOM legend uses font-variant-numeric. Canvas text has no such setting, so on the axes and in the canvas legend (screenshots) the chart places each digit in a fixed-width cell when the font's digits differ in width. Fonts whose digits are already tabular draw exactly as before. Other canvas surfaces use the font's own digits.

Loading fonts ​

The chart does not load fonts. Canvas text does not start a web font download, and text drawn before the font arrives uses a fallback. Load the font first, then apply it:

ts
await Promise.all([document.fonts.load('12px "Brand Sans"'), document.fonts.load('500 14px "Brand Sans"')]);
chart.applyOptions({ theme: { typography: { fontFamily: '"Brand Sans", system-ui, sans-serif' } } });

Load each weight you use. To swap a font that finishes loading later, listen for document.fonts.onloadingdone and call chart.invalidate().

Typography tokens ​

--pc-chart-font and --pc-chart-font-size set the baseline family and size. --pc-chart-font-weight (a number, normal or bold) and --pc-chart-numerals (tabular or proportional) set the baseline weight and numerals; chartTypographyTokens names them in code. Surface blocks have no tokens; set them in JS. In the UI shell, a typography block in chart.theme refines the one read from the tokens instead of replacing it.

Grid ​

ts
chart.applyOptions({ grid: { vertical: { visible: false }, horizontal: { lineStyle: 'dotted', color: 'rgba(128, 128, 140, 0.25)' } } });

Each direction, horizontal (price lines) and vertical (time lines), takes:

FieldDefaultNotes
visibletrueDraw this direction's lines
colorthemeThe theme's gridHorizontal / gridVertical (tokens --pc-chart-grid-h / --pc-chart-grid-v), else grid
lineStyle'solid''solid', 'dashed' or 'dotted'

createChart and applyOptions accept a partial grid at any depth and merge it into the current one; color: undefined goes back to the theme color. Screenshots include the grid as shown.

Reading and persisting colors ​

chart.appearance() returns the resolved colors: { theme, grid }, with each grid direction's color filled in. The appearance:change event fires with the same object whenever applyOptions changes the theme or the grid, so an app can show the current values and save them.

ts
chart.on('appearance:change', ({ theme, grid }) => save({ up: theme.up, grid }));

Chart colors from CSS ​

themeFromCss(el) builds a ChartTheme from the tokens on an element. It reads them through computed style, so inherited values and var() references work.

ts
import { createChart, themeFromCss } from '@chart-on/core';

const chart = createChart(el, { theme: themeFromCss(el) });
// After changing the variables:
chart.applyOptions({ theme: themeFromCss(el) });
css
#my-chart {
  --pc-chart-bg: #0f172a;
  --pc-up: #22c55e;
  --pc-down: #ef4444;
  --pc-heatmap: #1e1b4b, #6d28d9, #22d3ee, #facc15;
}

A token that is missing, or not a color the renderer can use, keeps the value from the fallback theme. The fallback is the second argument and defaults to darkTheme: themeFromCss(el, lightTheme). The first argument can also be any object with getPropertyValue, such as a CSSStyleDeclaration.

The UI shell does this for you (see below).

UI shell ​

Every UI color, radius, spacing and font is a --pc-* CSS variable on .pc-shell, next to the chart tokens above. Override them on the shell element, for example through a class:

css
.my-theme.pc-shell {
  --pc-accent: #0ea5e9;  /* --pc-focus, --pc-line-series and --pc-drawing follow */
  --pc-accent-soft: rgba(14, 165, 233, 0.16);
  --pc-up: #22c55e;     /* the canvas follows too */
  --pc-down: #ef4444;
}
ts
createChartShell(el, { className: 'my-theme' });

The shell reads the chart tokens with themeFromCss when it is created, and once more on the next animation frame, so a shell created before its container is attached still picks them up. It reads them again when the shell element's class, data-theme or inline style changes, and when the OS scheme changes in system mode. So shell.el.style.setProperty('--pc-up', '#22c55e') recolors the canvas straight away. For changes the shell cannot see, such as a new stylesheet or a class on an ancestor, call shell.refreshTheme().

A theme in the chart options wins over the CSS, and the user's appearance overrides (below) win over both: user > JS > CSS > built-in theme.

ts
createChartShell(el, { chart: { theme: { up: '#22c55e' } } }); // --pc-up is ignored

Appearance panel ​

The shell lets users change chart colors, each with its own opacity, in an Appearance panel: Candles (body, wick and border for rising and falling bars), Background, Grid (each direction's visibility, line style and color), Scales and text, Crosshair and Watermark. Changes preview live; each section has a Reset, and Reset all drops every override. Open it from a toolbar button or from code:

ts
import { appearanceToolbarItem, createChartShell } from '@chart-on/ui';

const shell = createChartShell(el, { toolbar: [/* ... */ appearanceToolbarItem()] });
shell.appearance?.open();

The choices are overrides on top of the theme, so switching between dark, light and system keeps them and updates every other color. They are saved to localStorage (charton:appearance) and restored on the next visit. With several charts on one page, give each its own storageKey, or they share one set of choices and overwrite each other. Pass your own storage, or null to keep them for the current visit only; appearance: false turns the feature off:

ts
createChartShell(left, { appearance: { storageKey: 'my-app:appearance:left' } });
createChartShell(el, {
  appearance: { storage: { load: () => loadFromServer(), save: (patch) => saveToServer(patch) } },
});

Saved choices are checked when they are loaded, from any storage: unknown keys and values that are not usable colors are dropped.

shell.appearance also sets overrides from code: set({ theme: { up: '#22c55e' }, grid: { vertical: { visible: false } } }) merges them (an undefined value removes one), reset('candles') or reset() drops them, and overrides reads them. Colors changed through chart.applyOptions show up in an open panel straight away, and a later change in the panel only touches the keys it overrides, so it does not undo them.

The swatch-and-opacity picker is createColorControl({ label, value, onInput, onCommit }), for use in your own settings UI: onInput fires on every change for a live preview, onCommit once when the picker closes after a change. Without swatches, it offers defaultSwatches: a neutral row, then rows of ten hues from light to dark, one hue per column. The grid is one Tab stop; the arrow keys move by swatch and by row, Home and End jump to the ends. Outside the shell, createAppearance(chart, { baseTheme, storage, storageKey, presets, onOpenChange }) adds overrides, presets and the panel to any chart; onOpenChange(open) reports when the panel opens or closes, e.g. to update a toolbar button. isColor(value) from @chart-on/core checks a color before you store or apply it.

VariableRole
--pc-shellOuter frame behind the panels
--pc-canvas, --pc-panelChart card, toolbar, rail and bottom bar panels
--pc-surface, --pc-popoverControls, menus, flyouts
--pc-hover, --pc-activeHover and selected backgrounds
--pc-line, --pc-line-strongBorders
--pc-inkPrimary text
--pc-muted-strong, --pc-muted, --pc-faintSecondary text
--pc-accent, --pc-accent-ink, --pc-accent-softActive tool, selected options
--pc-focusFocus outline
--pc-shadowMenu and flyout shadow
--pc-tip-bg, --pc-tip-ink, --pc-tip-shadowTooltips
--pc-font, --pc-font-monoFonts; default to the platform UI font (system-ui) and monospace font (ui-monospace)
--pc-font-size-xs, --pc-font-size-sm, --pc-font-size, --pc-font-size-lg, --pc-font-size-xlFont sizes: 11, 12, 13, 14 and 15px
--pc-radius-xs, --pc-radius-sm, --pc-radius-md, --pc-radius-lgCorner radii
--pc-space-1 … --pc-space-6Spacing scale: 2, 4, 6, 8, 12 and 16px
--pc-rail-buttonRail button size
--pc-gapSpacing between panels
--pc-panel-radiusCorner radius of floating parts and the chart (see Panel styles); default --pc-radius-lg
--pc-panel-insetSpace between floating parts and the shell's edges; default 4px
--pc-easeEasing for transitions

Defaults for both schemes are in packages/ui/src/style.css. --pc-chart-bg defaults to var(--pc-canvas), so the card and the chart stay the same color.

Colour presets ​

The top of the Appearance panel has a Presets section: one click restyles the chart and the shell chrome together. Each preset has a light and a dark variant, each a background and a primary colour; everything else is derived from those two. The built-in ten:

PresetLight backgroundLight primaryDark backgroundDark primary
Paper & Ink#F5F1E8#242424#242424#F5F1E8
Matcha Cream#F7F3E8#526B47#161B14#A9C79A
Lavender Mist#EDE8FF#403578#1A1726#C9BBFF
Butter Cobalt#FFF2C6#2849C7#10162E#FFE38A
Peach Espresso#FFE5D5#382923#231915#FFC7A6
Terminal Lime#F3F7EA#3F6B12#151A16#CBF567
Midnight Violet#F1EEFB#5A3FC0#171426#B6A2FF
Obsidian Gold#F6F3EC#8A6416#181818#E8BD69
Deep Ocean#EAF4F7#0B6E86#101F2B#62D6E8
Carbon Coral#F6F2F3#C2463A#1E1B20#FF8D82

Paper & Ink is the default until the user picks something else. The variant follows the shell's resolved theme: switching light and dark, or the OS switching under system, keeps the preset and shows its other variant. The Light and Dark buttons under the swatches switch the shell theme, so a user can try both. Surfaces, borders, grid, text, crosshair, labels, watermark and the heatmap ramp are mixed from the background, so text keeps at least 7:1 and muted text 5:1 contrast; line series, drawings and the accent come from the primary. Up and down keep the current palette, so the colour-safe pair stays on. The Default entry goes back to the built-in theme. While the shell has the pc-theme-high-contrast class, the preset stands aside so the high-contrast colours show; it comes back when the class goes. Colours the user changes afterwards layer over the preset as usual, and a section's Reset restores the preset's value. The chosen preset, Default included, is saved with the other appearance choices; an id that is no longer offered falls back to the default theme.

appearance.presets replaces the list or turns it off, and appearance.defaultPreset picks the starting preset (null for the plain theme). Mix the exported built-ins with your own; a variant's overrides fixes chart colours and tokens shell variables the derivation gets wrong:

ts
import { builtInColorPresets, type ColorPreset } from '@chart-on/core';

const brand: ColorPreset = {
  id: 'brand',
  label: 'Brand',
  light: { background: '#fbf7ef', primary: '#a25d00' },
  dark: {
    background: '#0f1720',
    primary: '#f5a524',
    overrides: { crosshair: '#f5a524' },
    tokens: { '--pc-shell': '#0a0f15' },
  },
};
createChartShell(el, { appearance: { presets: [brand, ...builtInColorPresets], defaultPreset: 'brand' } });
createChartShell(other, { appearance: { presets: false } }); // no section, saved preset ignored

From code, shell.setColorPreset('deep-ocean') applies one and setColorPreset(null) goes back to the default theme; shell.colorPreset is the current id, and the shell emits preset:change with it. Without the shell, presetTheme(preset, mode) from @chart-on/core gives the full chart theme of one variant (pass a third argument to start from your own base theme, such as one with colorSafeUpDown), and presetShellTokens(preset, mode) from @chart-on/ui the matching --pc-* variables:

ts
import { builtInColorPresets, presetTheme } from '@chart-on/core';

chart.applyOptions({ theme: presetTheme(builtInColorPresets[0], 'dark') });

Scrollbars ​

Every scrolling area in the shell (menus, dialogs, lists, panels) draws its scrollbar from these tokens. The thumb colours default to the border and text colours above, so they follow the dark, light and high-contrast themes on their own.

VariableDefaultRole
--pc-scrollbar-size10pxTrack thickness
--pc-scrollbar-radiusvar(--pc-scrollbar-size)Thumb corner radius (fully rounded)
--pc-scrollbar-tracktransparentTrack color
--pc-scrollbar-thumbvar(--pc-line-strong)Thumb at rest; var(--pc-muted) in high contrast
--pc-scrollbar-thumb-hovervar(--pc-faint)Thumb under the pointer; var(--pc-ink) in high contrast
--pc-scrollbar-thumb-activevar(--pc-muted)Thumb while dragged; var(--pc-ink) in high contrast
css
.my-theme.pc-shell {
  --pc-scrollbar-size: 6px;
  --pc-scrollbar-thumb: rgba(14, 165, 233, 0.4);
}

Browsers that style scrollbars through pseudo-elements use all six tokens; the thumb sits 2px inside the track. Browsers with only the standard scrollbar-width and scrollbar-color properties draw their own thin scrollbar in the track and thumb colours, and use the hover colour while the pointer is over the scrolling area. Lists whose content changes while open (search results, the object tree, dialog bodies, the table view) reserve the scrollbar's space, so their width does not jump when a scrollbar appears.

To keep the platform's own scrollbars, add the pc-native-scrollbars class to the shell (createChartShell(el, { className: 'pc-native-scrollbars' })). In forced-colours mode the shell always uses the platform's scrollbars.

Accessibility presets ​

Two presets for users who need more than the default palette.

High contrast. highContrastDarkTheme and highContrastLightTheme are complete themes: text and muted text reach at least 7:1 on the canvas (WCAG AAA), price labels at least 4.5:1, and every series, line and grid separator at least 3:1. Their heatmap ramps keep the same lightness ordering as the default ones, with bigger steps.

To check your own colors, contrastRatio(foreground, background) from @chart-on/core returns the WCAG ratio (1 to 21), blending a translucent foreground over the background first. Normal-size text needs 4.5 for AA, 7 for AAA. The theme builder shows these ratios as you edit.

Colour-safe up and down. colorSafeUpDown.dark and colorSafeUpDown.light replace the rising and falling colours (and the ask line, which follows the down colour) with a blue and orange pair that also differs in lightness. It stays clearly apart for protanopia, deuteranopia and tritanopia, where the default mint and coral get closer. Merge the one matching your canvas into any theme, including the high-contrast ones:

ts
import { colorSafeUpDown, highContrastDarkTheme } from '@chart-on/core';

chart.applyOptions({ theme: colorSafeUpDown.dark });
chart.applyOptions({ theme: { ...highContrastDarkTheme, ...colorSafeUpDown.dark } });
PairUpDown
colorSafeUpDown.dark#4ba3ff#ff9a1f
colorSafeUpDown.light#1a62c9#c25a00

Colour is not the only cue: the hollow candle style (see chart types) draws rising candles as outlines and falling ones filled, which reads with any palette and in greyscale.

With the UI shell, the same presets are token sets in the stylesheet:

ts
const shell = createChartShell(el, { className: 'pc-theme-high-contrast', palette: 'color-safe' });
shell.palette; // 'color-safe'
shell.on('palette:change', ({ palette }) => savePreference(palette));
shell.setPalette('default');
  • The pc-theme-high-contrast class switches the chrome and the chart to high contrast. It follows data-theme (and the OS setting in system mode) for dark or light. Add or remove the class at any time; the shell notices and recolours the chart.
  • palette: 'color-safe' (or setPalette) sets data-pc-palette="color-safe" on the shell, which swaps the up, down and ask tokens for the pair matching the current scheme, over any theme. palette:change fires when setPalette changes it.

The demo has both switches under View → Accessibility.

Forced colours. When the operating system's high-contrast mode is on (forced-colors: active), browsers recolour the page with a small system palette, but not canvas content. forcedColorsTheme(base, container?) builds a theme from those system colours, resolved to values the chart can draw: Canvas background, CanvasText text, scales and falling bars, LinkText rising bars and lines, Highlight labels and selection, and the grid, watermark and heatmap as CanvasText at lower opacity. Rising and falling bars take the one pair every high-contrast palette keeps apart; add hollowDown: true to the candles so direction also reads from shape. It returns null when the colours cannot be read (no DOM).

The UI shell applies it on its own while the mode is on, over the app's theme and the user's colour overrides, and switches back when it turns off (theme:change fires both ways). Its stylesheet turns focus rings and panel edges, drawn with shadows elsewhere, into outlines, and marks pressed and selected controls with Highlight. Without the shell, follow the media query yourself:

ts
import { darkTheme, forcedColorsTheme } from '@chart-on/core';

const forced = matchMedia('(forced-colors: active)');
const applyForced = () => chart.applyOptions({ theme: (forced.matches && forcedColorsTheme(darkTheme)) || darkTheme });
forced.addEventListener('change', applyForced);
applyForced();

To check it, emulate forced-colors: active in the browser's developer tools (rendering settings).

Light, dark and system ​

The shell selects the scheme with a data-theme attribute on .pc-shell:

theme option / setThemeAttributeResult
'dark'data-theme="dark"Dark tokens
'light'data-theme="light"Light tokens
'system' (default)noneLight or dark from prefers-color-scheme, live
ts
const shell = createChartShell(el, { theme: 'system' });
shell.theme;          // 'system'
shell.resolvedTheme;  // 'light' or 'dark', what is on screen
shell.on('theme:change', ({ theme, resolved }) => savePreference(theme));
shell.setTheme('dark');

theme:change fires when setTheme changes the theme, when the OS scheme changes while the theme is system, and when re-reading the CSS tokens (see above) changes the chart colors. Use it to rebuild anything derived from the theme, such as per-point colors.

To restyle one scheme, override its tokens in a stylesheet loaded after @chart-on/ui/style.css. Cover system mode too, which has no attribute:

css
.pc-shell[data-theme='light'] {
  --pc-canvas: #fafafa;
}
@media (prefers-color-scheme: light) {
  .pc-shell:not([data-theme]) {
    --pc-canvas: #fafafa;
  }
}

The older .pc-light class still selects the light tokens, but the shell no longer adds it. Use data-theme in new code.