Design tokens in, modern CSS out

One typed config → light-dark(), @property, oklch ramps. Zero runtime. This page is styled by its own output — pencil on paper by day, chalk on board by night.

“System” follows your OS — pure CSS via color-scheme, no JS involved in the switch.

Why not just write the CSS?

Tokens drift

Hand-written --vars go stale: rename one, grep, pray. Here a token is a typed key — rename it and every use is a compile error, not a silently dead variable.

Dark mode is a ritual

Duplicated [data-theme] blocks, a FOUC hack, a JS listener for the system preference. light-dark() output needs none of it — the browser does the switching.

Pipelines are heavy

Style Dictionary wants a build step and a config folder. This is one function call, 2.6 kB gzip, works with any bundler or none.

Palettes for free

Ten shades from one brand color via oklch relative colors — computed by the browser, not shipped in your bundle. Rebrand = change one value.

Start in 30 seconds

One command scaffolds the config and writes the CSS. Everything below on this page is what it automates:

  zsh
$ npx varth init

var(−th) v0.2.0

 varth.config.ts scaffolded
 6 tokens · 2 themes · strategy: light-dark
 varth.css  0.81 kB · gzip 0.29 kB
 varth.js  1.06 kB · setTheme/getTheme, zero deps
 varth.d.ts  0.29 kB · types for the module above

  light  ██ accent  ██ bg  ██ text  ██ muted
  dark   ██ accent  ██ bg  ██ text  ██ muted

 done in 24 ms

next steps
  1. import "./varth.css"
  2. background: var(--th-accent) anywhere
  3. import { setTheme } from "./varth.js" typed, persisted, optional

In → out

Left: the actual config of this page. Right: the CSS varth gen writes — colors folded into light-dark(), the shadow (not a color) got an automatic media-query fallback, radius emitted once.

varth.config.ts

          
varth.css

          

One command → three files

npx varth gen reads the config and writes everything your app needs. The files are yours: commit them, read them, no runtime package in your bundle.

varth.css · the themes
:root {
  color-scheme: light dark;
  --th-accent: light-dark(#3d6fb4, #7fa9e0);
  --th-bg: light-dark(#fdfcf7, #212932);
}

[data-theme="light"] { color-scheme: light; }
[data-theme="dark"] { color-scheme: dark; }
varth.js · the switcher, ~1 kB
// theme names baked in, zero deps
const NAMES = ["light", "dark"];

export const setTheme = (theme) => { … };
export const getTheme = () => { … };

// re-applies the saved choice on import
varth.d.ts · the types
export type ThemeName = "light" | "dark";

export declare function setTheme(
  theme: ThemeName | "system",
): void;

// setTheme("drak") → compile error
your app · any framework or none
<link rel="stylesheet" href="varth.css">

.card { background: var(--th-surface); }

import { setTheme } from "./varth.js";
setTheme("dark"); // typed, persisted

<!-- force a theme on any subtree -->
<aside data-theme="dark">…</aside>

Live tokens

Everything below uses var(--th-*). Flip the theme — registered @property tokens transition smoothly.

Card

surface, text, muted, border, shadow, radius

Always light

data-theme="light" on this card only — scoped theming, no wrapper components.

Always dark

data-theme="dark"light-dark() flips per subtree via color-scheme.

Ramp: one color in, palette out

ramps: { brand: { base } } emits --th-brand-1..10 as oklch(from var(--th-brand) …) — the browser derives the shades. Pick a new base: