Theming
Every token is a custom property on :root. Override it in unlayered CSS; the components follow.
Custom properties
Defined once in TypeScript, generated into tokens.css, and read by every leaf through var(). The React stylesheet and vlak.css carry the same block. Light values below; dark where they differ.
| Property | Light | Dark | Does |
|---|---|---|---|
--bg | #FAF8F2 | #0E0C0A | Paper. The page and every surface. |
--text | #1A1A1A | #E8E8E8 | Ink. Type, borders of solid controls, focus rings. |
--text-secondary | #6B6B6B | #949494 | Gray. Labels, hints, table cells. |
--accent | #1A1A1A | #E8E8E8 | Aliases ink. There is no hue in the system. |
--divider | rgba(0,0,0,0.08) | rgba(255,255,255,0.10) | Hairlines: rules, frames, table rows. |
--divider-subtle | rgba(0,0,0,0.06) | rgba(255,255,255,0.07) | Fills only: hover, skeleton, muted. |
--grid-line | rgba(0,0,0,0.04) | rgba(255,255,255,0.05) | The quiet 204 verticals behind inner pages. |
--table-alt | rgba(0,0,0,0.02) | rgba(255,255,255,0.03) | Alternate rows, code block ground. |
--control-border | rgba(0,0,0,0.42) | rgba(255,255,255,0.38) | Form control boundary. 3:1 against the ground. |
--radius-sm | 4px | Buttons, boxes, dialogs, sheets. | |
--radius | var(--radius-sm) | Alias of --radius-sm. | |
--radius-chrome | 0px | Page chrome, cards, icon marks, charts. | |
--radius-in | max(0px, calc(var(--radius) - var(--pad))) | Concentric inner corner. | |
--pad | 20px | Box padding. 25px at or under 480px. | |
--gutter | 20px | Gap between modules. | |
--grid-size | 204px | The module: 184 column + 20 gutter. | |
--grid-image | linear-gradient(…) | The module verticals as a repeating gradient. | |
--grid-pos | 20px 0 | Where the gradient starts. | |
--hit | 40px | Minimum hit target. 44px at or under 640px. | |
--control-h | 40px | Control height. 44px on the phone. | |
--control-fs | 14px | Control type size. 16px on the phone. | |
--control-label | 12px | Field label size. 15px on the phone. | |
--duration-snap | 0.12s | State changes the user caused. | |
--duration | 0.2s | Easing changes. | |
--duration-confirm | 0.16s | Confirmations. | |
--ease | cubic-bezier(0.3, 0, 0.2, 1) | The one curve. | |
--transition | background-color, color | The default transition pair. | |
--text-scale | 1 | Multiplier on reading type. Chrome stays put. |
Overriding tokens
/* your.css, unlayered: it wins over vlak.tokens */
:root {
--bg: #FFFFFF;
--radius-sm: 8px;
}
[data-theme="dark"] {
--bg: #000000;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) { --bg: #000000; }
}Vlak's tokens sit in the vlak.tokens cascade layer. Author CSS outside any layer beats it, so a plain :root rule is the whole override. Set the dark value under [data-theme="dark"] and, if you rely on the system preference, under the media query too. The palette is neutral by design; there is no accent token that takes a hue, and the test suite fails on one.
data-theme and color-scheme
Three states. No attribute: the system preference applies through prefers-color-scheme. data-theme="dark": dark. data-theme="light": light, even on a dark system. Each block also sets color-scheme, so native selects, scrollbars, and form chrome match the page.
<script>
(function () {
var t = localStorage.getItem("vlak-theme");
var dark = t === "dark" || (!t && matchMedia("(prefers-color-scheme: dark)").matches);
if (dark) document.documentElement.dataset.theme = "dark";
})();
</script>To persist a choice without a flash, set the attribute before first paint. The ThemeToggle component stores its choice under vlak-theme in localStorage and sets the same attribute; the script above reads it back.
The module grid
/* Paint the module behind a page. base.css does this on body. */
.page {
background-image: var(--grid-image);
background-size: var(--grid-size) 100%;
background-position: var(--grid-pos);
}
/* Boxes span whole modules: n × 204 − 20. */
.two-up { width: calc(2 * var(--grid-size) - var(--gutter)); }204px modules: a 184px column and a 20px gutter. Content boxes span whole modules; edges step from grid line to grid line on resize. At or under 480px the field is two columns on 25px gutters and --pad becomes 25px. Turn the verticals off with --grid-image: none.
Text scale
html { --text-scale: 1.1; } /* steps: 0.9, 1, 1.1, 1.25, 1.4 */Reading type (rs-t-*) multiplies by --text-scale; controls, labels, and chrome stay put. The type scale is in rem with no root font-size pin, so a reader's browser setting applies as well.
Control scale
Desktop controls are 40px tall with 14px type. At or under 640px every interactive control recuts to a 44pt hit with 16px type, through --hit, --control-h, and --control-fs. Override those three to change the whole kit.