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
| Export | Look |
|---|---|
darkTheme | Default dark |
lightTheme | Light |
neutralTheme | Neutral graphite |
highContrastDarkTheme | Black canvas, white text, stronger lines (see Accessibility presets) |
highContrastLightTheme | White canvas, black text, stronger lines |
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.
| Role | Dark | Light |
|---|---|---|
| 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:
| Theme | Low → 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.
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;
}| Key | CSS token | Used for |
|---|---|---|
background | --pc-chart-bg | Chart background |
backgroundBottom | --pc-chart-bg-bottom | Bottom of a vertical background gradient from background; optional, solid background when unset. Screenshots include it |
grid | --pc-chart-grid | Grid lines |
gridHorizontal, gridVertical | --pc-chart-grid-h, --pc-chart-grid-v | Per-direction grid colors; optional, default to grid |
separator | --pc-chart-separator | Pane separators |
text | --pc-chart-text | Axis labels, legend |
textMuted | --pc-chart-text-muted | Secondary axis and legend text |
fontFamily | --pc-chart-font | Font of all chart text, unless typography overrides it |
fontSize | --pc-chart-font-size | Size of chart text in px, unless typography overrides it |
typography | --pc-chart-font-weight, --pc-chart-numerals | Weight, numerals and per-surface fonts; see Typography |
crosshair | --pc-chart-crosshair | Crosshair lines |
labelBackground | --pc-chart-label-bg | Crosshair and last-value labels |
labelText | --pc-chart-label-text | Text on those labels |
up, down | --pc-up, --pc-down | Candle colors |
upWick, downWick | --pc-up-wick, --pc-down-wick | Candle wicks; optional, default to up / down |
upBorder, downBorder | --pc-up-border, --pc-down-border | Candle body outlines; optional, none when unset |
line | --pc-line-series | Default line and histogram color |
drawing | --pc-drawing | Drawing tools |
drawingSelected | --pc-drawing-selected | Default color of text drawings |
drawingFill | --pc-drawing-fill | Drawing fills |
referenceLine | --pc-chart-reference-line | Previous close, visible high, low and average reference lines |
bid, ask | --pc-bid, --pc-ask | Bid and ask lines |
depthBid, depthAsk | --pc-depth-bid, --pc-depth-ask | Depth lane bars; optional, default to bid and ask |
extendedHours | --pc-extended-hours | Pre and post session price label |
alert | --pc-alert | Alert lines |
timeMark | --pc-time-mark | Badges of the data source's time-axis markers; optional, defaults to line |
annotationFill | --pc-annotation-fill | Default fill of time bands and price zones; keep it translucent |
heatmap | --pc-heatmap | Heatmap color ramp, low to high, as a comma-separated list |
watermark | --pc-chart-watermark | Watermark 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-purple | Named 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:
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.
| Surface | Text |
|---|---|
axis | Price and time axis ticks, and the price, crosshair and countdown labels on the axes |
legend | Legend rows and the heatmap inspector |
labels | Labels inside the plot: price lines, alerts, orders, trade badges, tooltips and indicator messages |
drawings | Drawing labels, and the default font of text drawings |
watermark | Watermark family and weight; sizes come from the watermark options |
markers | Text 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:
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
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:
| Field | Default | Notes |
|---|---|---|
visible | true | Draw this direction's lines |
color | theme | The 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.
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.
import { createChart, themeFromCss } from '@chart-on/core';
const chart = createChart(el, { theme: themeFromCss(el) });
// After changing the variables:
chart.applyOptions({ theme: themeFromCss(el) });#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:
.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;
}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.
createChartShell(el, { chart: { theme: { up: '#22c55e' } } }); // --pc-up is ignoredAppearance 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:
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:
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.
| Variable | Role |
|---|---|
--pc-shell | Outer frame behind the panels |
--pc-canvas, --pc-panel | Chart card, toolbar, rail and bottom bar panels |
--pc-surface, --pc-popover | Controls, menus, flyouts |
--pc-hover, --pc-active | Hover and selected backgrounds |
--pc-line, --pc-line-strong | Borders |
--pc-ink | Primary text |
--pc-muted-strong, --pc-muted, --pc-faint | Secondary text |
--pc-accent, --pc-accent-ink, --pc-accent-soft | Active tool, selected options |
--pc-focus | Focus outline |
--pc-shadow | Menu and flyout shadow |
--pc-tip-bg, --pc-tip-ink, --pc-tip-shadow | Tooltips |
--pc-font, --pc-font-mono | Fonts; 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-xl | Font sizes: 11, 12, 13, 14 and 15px |
--pc-radius-xs, --pc-radius-sm, --pc-radius-md, --pc-radius-lg | Corner radii |
--pc-space-1 … --pc-space-6 | Spacing scale: 2, 4, 6, 8, 12 and 16px |
--pc-rail-button | Rail button size |
--pc-gap | Spacing between panels |
--pc-panel-radius | Corner radius of floating parts and the chart (see Panel styles); default --pc-radius-lg |
--pc-panel-inset | Space between floating parts and the shell's edges; default 4px |
--pc-ease | Easing 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:
| Preset | Light background | Light primary | Dark background | Dark 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:
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 ignoredFrom 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:
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.
| Variable | Default | Role |
|---|---|---|
--pc-scrollbar-size | 10px | Track thickness |
--pc-scrollbar-radius | var(--pc-scrollbar-size) | Thumb corner radius (fully rounded) |
--pc-scrollbar-track | transparent | Track color |
--pc-scrollbar-thumb | var(--pc-line-strong) | Thumb at rest; var(--pc-muted) in high contrast |
--pc-scrollbar-thumb-hover | var(--pc-faint) | Thumb under the pointer; var(--pc-ink) in high contrast |
--pc-scrollbar-thumb-active | var(--pc-muted) | Thumb while dragged; var(--pc-ink) in high contrast |
.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:
import { colorSafeUpDown, highContrastDarkTheme } from '@chart-on/core';
chart.applyOptions({ theme: colorSafeUpDown.dark });
chart.applyOptions({ theme: { ...highContrastDarkTheme, ...colorSafeUpDown.dark } });| Pair | Up | Down |
|---|---|---|
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:
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-contrastclass switches the chrome and the chart to high contrast. It followsdata-theme(and the OS setting insystemmode) for dark or light. Add or remove the class at any time; the shell notices and recolours the chart. palette: 'color-safe'(orsetPalette) setsdata-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:changefires whensetPalettechanges 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:
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 / setTheme | Attribute | Result |
|---|---|---|
'dark' | data-theme="dark" | Dark tokens |
'light' | data-theme="light" | Light tokens |
'system' (default) | none | Light or dark from prefers-color-scheme, live |
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:
.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.