Vela, our next-generation charting platform, is live at vela.luxalgo.com.
LuxAlgo

Theming

Build a theme with createTheme: colors, design tokens, component settings, part-by-part CSS and the cascade layers that keep your styles in charge.

View as Markdown

A theme is data: the chart's colors and font, every design token, and the settings of each component. createTheme builds one from a few choices; applied to a host, it is written once per document (or shadow root) as a rule in the vela.theme cascade layer, keyed by the host's data-vela-theme attribute.

import { createTheme, ensureUIHost } from '@luxalgo/vela-ui/theme';

const theme = createTheme({
    base: 'dark',              // 'dark' (default), 'light', or a chart theme's seven fields
    accent: '#7c3aed',         // brand color; the ink on it is derived
    radius: 'sm',              // none | sm | md | lg: scales every radius token
    font: 'Inter, sans-serif',
    tokens: { 'surface-elev': '#1b1c22' },     // any token, by name
    components: { button: { radius: '2px' } }, // a component's own settings
});

ensureUIHost(document.getElementById('app')!, theme);

On a page of your own, ensureUIHost(el, theme) makes the element a kit host and themes it: the kit's base styles apply inside it, and menus, dialogs and tooltips opened from it mount there. applyTheme(el, theme) alone only themes an element, so a menu opened from it would mount outside, unthemed; use it to switch a host that is already one to another theme.

In Vela, pass the theme as the chart's theme option or to setTheme, and offer it in the chart settings with registerTheme({ id, label, theme }).

Themes from colors alone

colors builds a whole theme from a few colors: grid and border derive from the background and the text, and light or dark is read from the background.

const paper = createTheme({ colors: { background: '#fdf6e3', accent: '#268bd2' } });

up and down are the candles' colors, and the up, down, profit and loss tokens follow them. In Vela they are the candles' defaults: a chart's own upColor and downColor options, and colors a user picks in its settings, win over them.

A theme of colors only is plain JSON that cannot break the UI: users can save their own, an app can offer a picker, and an agent can write one. A theme file names the schema shipped in the package, so editors and CI check it:

{
    "$schema": "./node_modules/@luxalgo/vela-ui/dist/theme.schema.json",
    "colors": { "background": "#0f1117", "text": "#c9d1d9", "accent": "#7c3aed" }
}

The tokens

Every token is a custom property, --vela-<name>; in a theme, tokens takes the name without the prefix, and in a component's CSS t reads it (${t.fg.muted}).

GroupTokensWhat they set
Textfg, fg-muted, fg-faint, fg-bright, fg-on-fillbody text, secondary text, placeholders and disabled text, emphasized text, ink on a saturated fill
Surfacesbg, surface, surface-elev, surface-overlay, surface-sunkenthe chart's background, panels, menus and popovers (opaque, above the chart), recessed fields
Linesborder, border-strong, border-soft, border-faint, separator-hover-band, separator-hover-lineborders, field borders, rules inside a panel, a divider under the pointer
Stateshover, hover-strong, active, focus, focus-soft, selected-bg, selected-fg, highlighthover and pressed washes, the focus ring, the selected chip and its ink, favorites
Brand and meaningaccent, accent-bright, up, down, profit, loss, dangerthe brand color, the candles' colors, gains and losses, destructive actions
Depthshadow, shadow-dialog, backdropfloating surfaces, dialogs, the dim behind a dialog
Scrollbarsscroll, scroll-sizethe thumb's color (the chart's border color by default) and the bar's width (5px)
Scalespace-1…space-4, radius-sm, radius-md, radius-lg, font, font-size-sm, font-size-md, font-size-lg, dur-fast, dur-med, ease, z-tooltip, z-menu, z-dialog, z-popoverspacing, corners, type, motion and stacking, the same in every theme

The colors derive from the chart theme's seven fields, so a theme that sets only those (or only colors) still sets every token.

Component settings

Each component declares a few settings with defaults that point at shared tokens (the button's radius defaults to radius-md), so a theme that sets nothing looks exactly as before. Each is written as --vela-<component>-<setting>, so a host can also set one in its own CSS. The components field is typed from the declarations: a component or a setting that does not exist fails the typecheck. dist/manifest.json lists every component's settings.

Restyling a component completely

When settings are not enough, a component's css restyles its parts: shape, typography, borders, shadows, every variant and state. It is keyed by the parts the component declares:

import { createTheme, css, t } from '@luxalgo/vela-ui';

const theme = createTheme({
    components: {
        button: {
            radius: '999px',
            css: {
                root: css`
                    text-transform: uppercase;
                    box-shadow: ${t.shadow};
                    &[data-variant='outline'] { border-width: 2px; }
                    &:hover:not(:disabled) { transform: translateY(-1px); }
                `,
                icon: css`order: 1;`,
            },
        },
    },
});
  • The name and the part pick the element. components.button.css.icon becomes a rule on [data-slot~='button-icon']; & is that part, so its variants and states follow it.
  • Scoped to the theme. Each part's rule sits under the theme's data-vela-theme, so two charts with two themes on one page each keep their own. Floating layers mount inside their themed host, so a select's list or a dialog's content is reached the same way.
  • In the last layer. It goes into @layer vela.custom, after every layer of the kit, so it wins without !important; a page's unlayered CSS still beats it.
  • Flattened at run time, one level: & rules, rules without & (descendants), and @media, @supports or @container around either. @keyframes and @font-face belong in the page's stylesheet; the part uses them by name.
  • Written against the public contract: the parts, data-variant, data-size, and the state attributes (data-state, data-selected, data-disabled). Class names are not part of it; checkTheme flags them, and !important. The shared stylelint config (@luxalgo/vela-ui/stylelint) checks CSS written with the css tag in a project too.
  • Off for themes loaded from data. CSS that was not written with the css tag (a saved JSON, a shared file, a generated theme) is left out unless the host passes createTheme(input, { allowCss: true }). CSS from someone else can restyle the page to mislead and can load remote images that leak what the page shows. Colors and settings carry no such risk.

CSS changes how a component looks, not its markup or behavior.

Checking a theme

checkTheme(input) lists what a theme gets wrong: unknown tokens, a component, setting or part that does not exist (the kit's components, and those the page declared with defineComponent), colors the theme cannot derive from (it reads #rrggbb and rgb()), text with too little contrast on its background, ink with too little contrast on the accent, and part CSS that is left out or reaches past the component's contract. createTheme warns about each problem once. The typecheck catches the same names in TypeScript; this catches them in a theme loaded from JSON or written in plain JavaScript, where nothing else would.

Cascade layers

Every style the kit and Vela write sits in a cascade layer, declared once per document (or shadow root) in this order, lowest first:

LayerHoldsWritten by
vela.themethe tokens and the scaleapplyTheme, or a built-in theme's CSS file
vela.uithe kit's componentseach component, when it is first created
vela.chromeVela's own chrome: toolbars, legends, the status line, dialog layoutsVela
vela.proVela Pro's featuresVela Pro
vela.customa theme's component cssapplyTheme
no layeryour page's CSSyou

A later layer wins over an earlier one whatever the selectors' weight, and CSS in no layer wins over all of them, so a page overrides any of it without !important. Layers a page declared before the first Vela stylesheet rank below Vela's. Write CSS that restyles components into vela.custom to sit above the kit and below your page's other CSS.

A component of your own (built with the kit's helpers, or copied with vela-ui add --source) puts its base styles in vela.ui, like the kit's, so a theme's part CSS and your restyles reach it. In vela.custom its rules would tie with the theme's, and whichever stylesheet loaded last would win, usually the component's own.

Unlayered CSS also reaches the kit when you did not mean it to: a global reset such as button { background: none; } or input { border: 0; } now restyles the kit's buttons and fields. Scope such rules away from Vela (button:where(:not(.vela-ui *)) { … }), or move them into a layer declared before Vela's (@layer reset; first in your stylesheet).

Overriding from a page's CSS

The theme layer sits below every other style, so a page's own CSS overrides any token without !important. [data-vela-theme] reaches every themed host, chrome inside a chart included:

[data-vela-theme] { --vela-accent: #7c3aed; }

Scrollbars

Every scroller inside a kit host (.vela-ui) and its floating layers shows the same scrollbar: thin, a rounded thumb, no track and no arrows. scroll colors the thumb and scroll-size sets the bar's width; set them like any token:

createTheme({ tokens: { scroll: '#3b3f4a', 'scroll-size': '8px' } });

Chromium and Safari draw it from the ::-webkit-scrollbar parts, Firefox from scrollbar-width and scrollbar-color. Setting scrollbar-width or scrollbar-color on an element switches Chromium back to its native bar, so a page that sets them on every element (* { scrollbar-width: thin; }) brings the native bar back inside the chart too. Leave the kit out of that rule, or reset it:

.vela-ui, .vela-ui * { scrollbar-width: auto; scrollbar-color: auto; }

A scroller outside any kit host takes the same scrollbar with overlayScrollbarCss(selector) (@luxalgo/vela-ui/styles).

Without JavaScript

A page that uses the kit without building a theme can link a built-in theme as plain CSS; it sets every token on the page root:

<link rel="stylesheet" href="node_modules/@luxalgo/vela-ui/dist/themes/dark.css" />

Bundlers resolve @luxalgo/vela-ui/themes/dark.css and @luxalgo/vela-ui/themes/light.css.

On this page