Theming and adding a theme
One OxTheme object colours the chrome, the shadcn components and the canvas.
An OxTheme is a flat object of tokens: bg, surface, border, text, textMuted, accent, buy, sell, the candle palette and a dark flag. ChartShell writes each token as a --ox-* CSS variable on its root, and pushes the same values into the canvas, which can't read CSS.
| Token | CSS variable | Also feeds |
|---|---|---|
surface | --ox-surface | shadcn --popover, --card |
accent | --ox-accent | shadcn --primary, --ring |
textMuted | --ox-text-muted | shadcn --muted-foreground |
sell | --ox-sell | shadcn --destructive |
upColor / downColor | --ox-up-color / --ox-down-color | Candles on the canvas |
Try it
Adding a theme
import { createTheme, BUILTIN_THEMES, ChartShell } from "@option-x-tech/ox-charts";
// Derive one from a built-in base...
const acme = createTheme("dark", { name: "acme", accent: "#ff6a00", upColor: "#3ddc84" });
<ChartShell themes={[acme, ...BUILTIN_THEMES]} theme="acme" /* ... */ />- Keep every colour a 6-digit hex or
rgba(): the same values fill canvas pixels. - Set
darkcorrectly. It pickscolor-scheme, thedark:variant and the mode the theme belongs to. - A candle palette alone is a
ColorTheme. It overlays up/down colours and buy/sell onto whichever base is active. - Adding a built-in? Add it to
theme-contrast.test.ts, which enforces WCAG contrast.
Popups follow the shell
Dialogs and menus portal into the shell's own host, so two terminals on one page can wear different themes and popups stay visible in fullscreen.
Built-in candle colour themes
Generated from COLOR_THEMES in src/ui/theme.ts.
| Theme | Name | Up / down on dark | Up / down on light |
|---|---|---|---|
| OptionX | optionx | #74d10a#ff4d5e | #619920#af1b3c |
| Terminal | terminal | #58d8ae#e95048 | #259c79#b11b1d |
| Voltage | voltage | #37e167#f72a71 | #24a047#ad1a4d |
| Meridian | meridian | #86c5fa#c57721 | #228dd6#895113 |
| Ultraviolet | ultraviolet | #c8aefa#a68921 | #9769dc#735e14 |
| Kiln | kiln | #f3b030#d86162 | #ad7c1f#a43237 |
| Harbour | harbour | #37d6dd#8979e6 | #24979d#5f4bb4 |
| Sage | sage | #8ecfa9#bb7582 | #579672#8b4957 |
| Graphite | graphite | #ededf0#6e6e78 | #8e8e96#3b3b44 |
| Dalian | dalian | #faa493#218c8e | #e85339#146364 |
Reference: how CSS and themes work
One OxTheme object drives everything that has a colour: the shell chrome
(toolbars, menus, dialogs), the shadcn/ui components, and the canvas engine.
Nothing is themed per component and there is no per-theme CSS.
OxTheme (src/ui/theme.ts)
├─ applyThemeVars() → --ox-* CSS variables on the shell root
│ └─ .ox-ui bridge → shadcn variables (--background, --primary …)
│ └─ Tailwind utilities (ox:bg-popover, ox:text-primary …)
└─ chartColorsOf() / orderflowColorsOf() / tradingPaletteOf()
→ hex colours pushed into the canvas engine1. How CSS and themes work
Tokens → CSS variables
OxTheme (src/ui/types.ts) is a flat object of tokens: bg, surface,
surfaceHover, border, text, textMuted, accent, buy, sell, the
chart colours, the candle palette, shadow, radius and font. It also has a
dark flag.
ChartShell resolves the active theme from its base (light/dark mode), the
chosen base theme and the colour theme (applyColorTheme). It then calls
applyThemeVars(root, theme) on its root element ([data-ox-shell]). That call
does three things:
- It writes every string token as
--ox-<kebab-name>, e.g.surfaceHoverbecomes--ox-surface-hover. - It sets
color-schemetodarkorlight. - It toggles
data-ox-dark. The Tailwinddark:variant keys off this attribute.
--ox-* is the only source of truth. Hand-written chrome styles read it
directly (style={{ background: "var(--ox-surface)" }} and the sheet in
src/ui/components/styles.ts).
The shadcn bridge
The shadcn/ui components (Base UI flavour, "Mira" preset, in src/ui/shadcn/)
are styled with shadcn's semantic variables. src/ui/styles/ox-ui.css maps each
of them onto an ox token, once, scoped to .ox-ui:
| shadcn variable | ox token |
|---|---|
--background | --ox-bg |
--foreground | --ox-text |
--card, --popover | --ox-surface |
--primary, --ring | --ox-accent |
--secondary, --muted, --accent | --ox-surface-hover |
--muted-foreground | --ox-text-muted |
--destructive | --ox-sell |
--border, --input | --ox-border |
--radius | --ox-radius |
The fallbacks in the bridge are the dark theme's values, used only by chrome
rendered outside a shell. Because the bridge is made of var() aliases, every
theme works as soon as its --ox-* values are on the root. That covers the
built-in bases, all colour themes, light and dark, and anything made with
createTheme().
Tailwind
@theme inline points Tailwind's colour and radius scales at the bridge
variables: ox:bg-popover compiles to background-color: var(--popover), and
ox:rounded-lg to border-radius: var(--radius). Popup surfaces also use the
tokens directly where shadcn has no equivalent:
ox:[box-shadow:var(--ox-shadow)] and ox:[font:var(--ox-font)].
This is a library that ships inside host apps, so the CSS is kept contained:
- No preflight. Only Tailwind's theme and utilities are compiled. A
zero-specificity reset in
ox-ui.csscovers shadcn parts ([data-slot]) inside.ox-uionly. - Prefixed utilities. Every class is
ox:-prefixed, so only prefixed candidates are generated and a host's own Tailwind never collides. - Nothing on
:root. The build moves Tailwind's variable declarations from:rootto.ox-ui, and renames the ones it declares from--ox-*to--oxtw-*, so they can never shadow a token. - Unlayered utilities. A host's unlayered element rules cannot outrank them.
The CSS is compiled ahead of time, so hosts need no Tailwind:
node tools/build-ui-css.mjs # regenerate src/ui/styles/ox-ui.generated.ts
node tools/build-ui-css.mjs --check # exit 1 if staleensureUiStyles() injects the result as a <style> tag on first use. Re-run
the build after adding or changing an ox: class; src/ui/styles/ui-css.test.ts
fails while the committed output is stale.
Popups and portals
ChartShell renders an OxPortalHost inside its root. Every dialog, menu,
popover, select and tooltip portals into that host, so it:
- inherits that shell's
--ox-*values, which keeps two charts on one page with different themes separate; - stays visible when the shell root is fullscreen;
- resolves the bridge, because the host carries
.ox-ui.
Nested popups (a Select in a dialog, a colour popover in the drawing settings) mount in the same host. Base UI handles their stacking and dismissal.
Menus, popovers and selects are modal. An outside press lands on Base UI's
transparent backdrop, which ox-ui.css lifts to the popover z-tier. The press
that closes a popup therefore never reaches the chart canvas, whether it comes
from a mouse, touch or pen.
2. Adding a theme
Add an OxTheme object. Nothing else is needed: no CSS, no component changes.
import { createTheme } from "@option-x-tech/ox-charts";
// Either derive one from a built-in…
const acme = createTheme("dark", { name: "acme", accent: "#ff6a00", upColor: "#3ddc84" });
// …or write a full OxTheme literal next to the built-ins in src/ui/theme.ts
// and list it in BUILTIN_THEMES / BASE_THEMES.
<ChartShell themes={[acme, ...BUILTIN_THEMES]} theme="acme" … />Keep every colour a 6-digit hex or rgba(), because the same values feed
canvas fills. Set dark correctly: it picks the color-scheme, the dark:
variant and the light/dark mode the theme belongs to. theme-contrast.test.ts
enforces WCAG contrast for the built-ins; add a new built-in there as well.
A candle palette alone is a ColorTheme (see COLOR_THEMES). It overlays
upColor, downColor, the wicks and buy/sell onto whichever base is
active.
3. Canvas colours come from the same tokens
The engine draws on <canvas>, where CSS variables cannot resolve, so it gets
the same theme as plain colour values:
chartColorsOf(theme)supplies the background, grid, axis text, crosshair, axis border and last-price colours. They are pushed withchart.applyOptions({ colors }), and candles useupColor,downColorand the wicks.orderflowColorsOf(theme)derives the footprint, volume profile, bubbles, TPO, POC and VWAP colours frombuy,sell,accent,text,pocandvwap.tradingPaletteOf(theme)supplies the order and position lines. It starts fromDEFAULT_TRADING_PALETTEorLIGHT_TRADING_PALETTEand applies the theme'stradingoverrides. It is deliberately not tied to the candle colours.
All of these read the resolved theme object that applyThemeVars receives. A
theme switch therefore recolours the chrome (through CSS variables) and the
canvas (through these mappings) in the same render. themeFromOxVars() goes the
other way: it builds an OxTheme from --ox-* variables a host already
publishes.
Automation config and limits
tradingOptions.automation: Auto mode, confirm timeout, script and manual limits.
Host integration
Embedding ox-charts in your app: symbol search and instrument specs, the trading adapter (including brokers that return no order id), ChartShell props, the trading dock, host theme, storage, and installing the package or linking its source.