•
3 min read
one token source, two themes, no dark prefix
ash-lumen css design-systems

Ash Lumen is the design system this site runs on, along with my habit tracker and flashcards app. Its rule is that every visual decision lives in token files, and a build compiles them into everything else. There is no dark: prefix anywhere in any of the three codebases, and the reason is a split in the token files that took a while to get right.

split by what varies

Three token files. base.json holds typography, spacing, radius, and transitions, the values that do not change with theme. light.json and dark.json hold colors and shadows only. The files follow the Design Tokens Community Group format, so each leaf has a value and a type, and other tooling can read them.

The build script, 150 lines of Node, compiles all three and writes three outputs: a CSS file of custom properties, an index that imports it alongside the component classes, and a JavaScript module for anything that needs a token value at runtime. Both themes generate the same custom-property names. A component written once against --color-bg and --color-accent is themed twice by definition.

two layers of resolution

Which theme’s values a property resolves to is decided in CSS, in two layers. A media query on prefers-color-scheme follows the operating system. A data-theme attribute on the root element overrides it when the user picks. Both layers set the same property names, so the override is settled by specificity inside CSS.

A component that needs a hand-written dark variant is a signal that a token is missing. That rule has held across three apps.

tailwind without a config file

Tailwind 4 is configured in CSS. An @theme inline block maps token names to utility classes without generating a second set of custom properties, which keeps the compiled token file the single source. The cost of that choice is that opacity modifiers such as /30 do not work on custom-property colors, so anywhere that needs a translucent token uses color-mix() instead.

the pipeline is the artifact

Consumers do not import compiled CSS. Each app has its own token files and its own build step, running the same script. That is what makes the system reusable: the habit tracker’s palette is not this site’s palette, but the compile, the property naming, and the two-layer resolution are identical. A design system nobody uses is a mood board, so it is published separately and this site is the demo.

monochrome keeps the token set small

None of the pipeline is novel. The reason it stays manageable is a constraint that has nothing to do with tooling: the system is monochromatic. Hierarchy comes from the gray spectrum, brightness differentiates importance, and hue is reserved for meaning, error, warning, success, and info, and even those are desaturated. Theming light and dark from one source is easy with about fifteen grays and four signal colors. It is much harder with sixty brand tints, and tokenizing a palette that was never designed to be systematic is where most of the difficulty in a token pipeline comes from.