The whole app re-skins from a few switches. Six axes from Design Tokens, mode, palette, accent, radius, density and font, are each a data-* attribute on <html>, or on a subtree, and every utility re-resolves when one changes, because the chain ends in the semantic tokens (--bg, --fg, --brand, --r, --pad) that datler-theme.css maps onto Tailwind’s names. A design choice is made as token values and axis options rather than one-off colours, so it stays themeable and consistent with the rest.
The runtime
apps/renderer/src/lib/theme.ts binds the library’s theme runtime, @jeliroco-multimedia/blunt-tokens/runtime, to Datler’s two particulars: its axis catalog (datlerThemeAxes, adapted once into the library’s shape as runtimeAxes, since Datler’s accent axis is data-accent where the library’s is data-brand) and its storage keys (datler:theme:data-mode and so on, the prefix datler:theme), so saved themes keep working. It is the one place both are bound, so no caller has to remember either.
- Boot.
apps/renderer/index.htmlcarries the six defaults on<html>inline, andapplyTheme()inmain.tsxlays any saved choices over them before the first render, so there is no flash of the wrong skin. - Persistence. Each axis is saved in the device’s
localStorageunder its key. A theme belongs to the device, not the profile, and is not synced. - System mode.
modestores a preference, dark, light or system. System resolves against the operating system’sprefers-color-schemebefore it reaches the attribute, and while the stored preference is system, a change in the operating system’s setting re-resolves it. The CSS only ever seesdata-mode="dark"or"light".
Where it is switched
Every surface renders from the same catalog, so none can offer an option the CSS lacks:
- Settings → Appearance (
routes/settings.index.tsx), a row per axis, built from the library’suseThemeAxesandThemeAxisOptions, each group named by its row’s label. - The Appearance popover (
components/theme/ThemeTweaks.tsx), opened bymod+,from anywhere (App Shell), the same pickers in a small panel at the top right. It traps focus without a backdrop, because the point of it is to watch the app re-skin live behind it. - The ⌘K palette (Frictionless Capture) offers each axis option as a theme action among its search results; choosing one sets it directly.
- Storybook’s toolbar, one control per axis (Storybook).
The accent also colours the mark in the header (The Datler Mark).
Data visualization and density
The token set includes the charts’ colours, so a chart themes with the app: a categorical series palette (--chart-1 to --chart-8), a sequential ramp (--seq-0 to --seq-4), a diverging scale (--div-neg, --div-mid, --div-pos, which the correlation matrix uses) and --grid. Touch targets and spacing are a density question, set by the density axis, which matters most for dense surfaces like the editor and quick capture.
Contrast
packages/design-tokens/contrast.test.ts holds every palette and mode legible: primary ink (--fg) at AAA (7:1) on every text surface, the other inks (--fg-muted, --fg-dim, --fg-faint) at AA (4.5:1) on every surface including the inset one, and the text on a filled button (--brand-ink on --brand) at AA for every accent. It reads every block for a palette, mode or accent in cascade order and takes a token’s last declaration, so an override Datler adds is what it measures. It also checks that every option in the catalog has a CSS block behind it.
It parses the composed theme, the library’s defaults.css and axes.css and then Datler’s accent blocks, in cascade order, not Datler’s file alone. That composition is Datler’s, and the library’s own tests cannot see an override added here, so this is the check that would catch one. The dimmer inks, --fg-dim and --fg-faint, take the library’s values; they have no floor in this test, though the library uses them for text such as nav labels and column headers. WCAG exempts disabled controls, not text a design calls subtle.
A complete token set is a correctness property, not tidiness. CSS treats var(--missing) with no fallback as invalid at computed-value time, so a forgotten token gives height: auto, padding: 0 or a transparent background with no console error, indistinguishable from a component bug. That is why the theme is held to the library’s whole token contract rather than to the subset anything reads today, and why the app checks it in development (see Design Tokens and Jeliroco Components).
Related
Design Tokens · Storybook · Jeliroco Components · The Datler Mark · Page Specs