Theming

Every token is a custom property on :root. Override it in unlayered CSS; the components follow.

Scoped token sandbox

Change the local scheme and control type scale. The rest of this page keeps its current theme.

Scoped preview · light · 14px control type

Only this framed preview receives the token overrides.

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.

PropertyLightDarkDoes
--bg#FAF8F2#0E0C0APaper. The page and every surface.
--text#1A1A1A#E8E8E8Ink. Type, borders of solid controls, focus rings.
--text-secondary#6B6B6B#949494Gray. Labels, hints, table cells.
--accent#1A1A1A#E8E8E8Aliases ink. There is no hue in the system.
--dividerrgba(0,0,0,0.08)rgba(255,255,255,0.10)Hairlines: rules, frames, table rows.
--divider-subtlergba(0,0,0,0.06)rgba(255,255,255,0.07)Fills only: hover, skeleton, muted.
--grid-linergba(0,0,0,0.04)rgba(255,255,255,0.05)The quiet 204 verticals behind inner pages.
--table-altrgba(0,0,0,0.02)rgba(255,255,255,0.03)Alternate rows, code block ground.
--control-borderrgba(0,0,0,0.42)rgba(255,255,255,0.38)Form control boundary. 3:1 against the ground.
--radius-sm4pxButtons, boxes, dialogs, sheets.
--radiusvar(--radius-sm)Alias of --radius-sm.
--radius-chrome0pxPage chrome, cards, icon marks, charts.
--radius-inmax(0px, calc(var(--radius) - var(--pad)))Concentric inner corner.
--pad20pxBox padding. 25px at or under 480px.
--gutter20pxGap between modules.
--grid-size204pxThe module: 184 column + 20 gutter.
--grid-imagelinear-gradient(…)The module verticals as a repeating gradient.
--grid-pos20px 0Where the gradient starts.
--hit44pxMinimum hit target. 44px at or under 640px.
--control-h44pxControl height. 44px on the phone.
--control-fs14pxControl type size. 16px on the phone.
--control-label12pxField label size. 15px on the phone.
--duration-snap0.1sState changes the user caused.
--duration0.2sEasing changes.
--duration-confirm0.3sConfirmations.
--easecubic-bezier(0.3, 0, 0.2, 1)The one curve.
--transitionbackground-color, colorThe default transition pair.
--text-scale1Multiplier on reading type. Chrome stays put.
/* 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.

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.

/* 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.

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.

Desktop controls are 44px 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.