Theming the View Engine
Not released
@ahoo-wang/wow-view-engine has not been published to npm and carries no compatibility promise. This page describes theming as it stands in the repository.
The theme is the host's look, not a way of observing: nothing about it is saved in a view, a dashboard or a preference, and the workbench has no theme switch. The host picks a preset and a mode; the engine follows. Everything below is CSS custom properties — there is no theme object and no provider.
Three stylesheets
| Entry | What it is | Import it when |
|---|---|---|
@ahoo-wang/wow-view-engine/styles.css | The theme: every rule scoped inside the view's own boundary, every colour a token that reads a host variable | Always |
@ahoo-wang/wow-view-engine/themes.css | The built-in presets, keyed by a data-fve-preset attribute | You want a preset |
@ahoo-wang/wow-view-engine/shadcn-bridge.css | Your shadcn/ui tokens read into the view's host variables | Your app already has a shadcn theme |
The two optional files only assign --fve-* variables: they paint nothing and never touch a variable of yours. The package's build checks that on every release.
Presets
import '@ahoo-wang/wow-view-engine/styles.css';
import '@ahoo-wang/wow-view-engine/themes.css';<html data-fve-preset="neutral">neutral is the theme's own look and the default when no preset is set. blue is the neutral greys with a blue primary; slate is cool greys with blue, the look of the compensation console. Each gives both a light and a dark half, and neither turns input or ring into the brand colour. The values each one sets are listed in the package README. The attribute on <html> reaches every view and every popup.
- A preset and the mode are independent. The preset supplies both halves of the values; light or dark is still decided as described under Light, dark and system.
- A preset never changes the chart colours,
pin-shadowortext-ui. See Chart colours. - Your own preset is written the same way and selected by the same attribute:
:where([data-fve-preset='acme']) { --fve-primary: …; --fve-dark-primary: …; }.
Host overrides
Every token reads a host variable with the built-in value as its fallback: --fve-<token> for light and --fve-dark-<token> for dark. Set them on your :root:
:root {
--fve-primary: oklch(0.55 0.21 265deg);
--fve-primary-foreground: oklch(0.99 0 0deg);
--fve-dark-primary: oklch(0.75 0.15 265deg);
--fve-dark-primary-foreground: oklch(0.21 0.05 265deg);
--fve-radius: 0.375rem;
}Presets and the bridge are written as :where(…), which weighs nothing, so a variable you set on :root wins over the preset you chose, whichever stylesheet loads first. Override one colour of a preset without restating the rest. The full token list is in the package README.
Light, dark and system
| How | Effect |
|---|---|
A .dark class on any ancestor, usually <html> | Views follow your page's mode |
theme="light" or theme="dark" on ViewSurface, a workbench or an embed | Pins that one view |
theme="system" | Follows the reader's prefers-color-scheme, live, for a page with no switch of its own |
Pinning a preset
preset="blue" on ViewSurface, a workbench or an embed pins that view to a preset, whatever <html> says. A data-fve-preset on any other ancestor works too: the surface finds the nearest one.
<EmbeddedView engine={engine} instanceId={id} theme="dark" preset="slate" />Embeds and popups
Menus, selects, popovers, tooltips and dialogs are portalled to <body>, outside the part of the page the view sits in. They carry what the surface resolved — its mode as data-theme and its preset as data-fve-preset — so a pinned embed's popups match it.
- Use an attribute, not a stylesheet swap. A chart reads its colours off the tokens and is told to read them again when a
class,data-theme,data-fve-presetorstyleattribute changes on the surface or an ancestor. A stylesheet replaced with no attribute changing leaves charts in the old colours. - Variables set on one element stop at
<body>.--fve-*set on a card around an embed reach the embed but not its popups, which are not inside the card. Put page-wide values on:root; usepresetfor one view. - An embed on a card paints
--background. Give it the card's colour on the card —--fve-backgroundand--fve-dark-background— rather thantransparent. - Stacking: popups paint at
z-index: 50; raise them all with--fve-popup-z-indexon:root.
The shadcn bridge
import '@ahoo-wang/wow-view-engine/styles.css';
import '@ahoo-wang/wow-view-engine/shadcn-bridge.css';The bridge points each --fve-<token> and --fve-dark-<token> at the shadcn token of the same name — background, foreground, card, popover, primary, secondary, muted, accent with their -foreground pairs, border, the five sidebar* tokens and radius. It is resolved on <html>, so it takes your values in the mode <html> is in.
| Not bridged | Why |
|---|---|
input, ring | A shadcn theme often writes --input: var(--border) and --ring: var(--primary): a divider grey and a brand colour that owe nothing of the 3:1 a control's edge and a focus mark need |
destructive, success, warning | Text colours measured to 4.5:1 in both modes; shadcn has no success or warning |
| The eight chart colours | shadcn palettes have five, often starting on red; these are measured for colour-vision distance |
row-hover, quiet-foreground | Derived from bridged tokens |
To adopt it in an app with an existing shadcn theme:
- Keep your
:rootand.darkblocks as they are, with.darkon<html>. - Import
styles.cssandshadcn-bridge.css. Drop anydata-fve-preseton<html>: the bridge and a preset there weigh the same, so use one. - Let views follow your page's mode. A view pinned to the opposite mode would read your current values in both halves.
- Override what you want beyond the bridge one variable at a time, for example
--fve-ring, and measure it (below). - Check your own text tokens: they are carried across as they are. A
--muted-foregroundthat misses 4.5:1 on your--backgroundmisses it in the views too.
A Storybook regression test (ShadcnBridge.test.stories.tsx) hangs the compensation console's shadcn theme on a workbench with the bridge and measures its control edges and focus in both modes.
Contrast is the overrider's responsibility
Every built-in preset holds these lines in both modes, measured in a real browser on every token pair:
| Line | Tokens |
|---|---|
| Text, ≥4.5:1 | Every *-foreground on its ground; muted-foreground on background, card and popover; foreground on muted and row-hover; quiet-foreground; destructive, success and warning as text on background and card |
| Controls and focus, ≥3:1 | input and ring on background, card and popover, and on a dark control's own input/30 wash |
| None | border and sidebar-border (dividers), radius, text-ui |
When you set --fve-ring or --fve-input — or their --fve-dark- halves — you take over the 3:1: an unticked checkbox is only its input edge, and a focused control is known by its ring edge. Setting --fve-primary or --fve-border leaves both alone.
Measure a theme of your own in the Storybook contrast matrix: paste your --fve-* declarations into its field and they are measured beside the built-in presets, pair by pair.
Chart colours
The eight chart slots are the same in every preset and are not bridged, so a series keeps its colour across themes. They are tuned for colour-vision distance between slots and for a legible label ink on every mark, in both modes. A host can still set --fve-chart-1 … --fve-chart-8 and their --fve-dark- halves; it then owes its palette those two measurements. Colours saved in a chart's own config are declared in code and do not follow the mode or the preset.
See it
The theme gallery shows every preset in light, dark and system mode: a dashboard with its filter bar, a record table panel and an analysis chart panel, the same records as cards, and an export dialog. The Storybook toolbar has a Preset switch and a system mode for every other story.