Designing the tokens
How a low-saturation moss green became a design system that checks its own contrast.
A thalweg is the line along a valley where the water runs deepest. When we picked a visual language for thalweg labs, we wanted the same quality: calm on the surface, deliberate underneath. This post walks through the decisions behind our design tokens.
Start with data, not pixels
Every color is described in OKLCH — lightness, chroma, hue. “Low saturation” stops being a
feeling and becomes a number: our primary, Moss, peaks at a chroma of 0.055.
Each palette is generated from just two values:
export const HUE_PALETTES = {
moss: { hue: 128, peakChroma: 0.055 },
jade: { hue: 160, peakChroma: 0.09 },
clay: { hue: 25, peakChroma: 0.12 },
};Changing the brand color means changing one line. The ramps, both color modes, the Tailwind theme and the contrast checks follow.
Why OKLCH
Equal steps in OKLCH lightness look like equal steps to the eye, so a generated ramp stays even across hues — something HSL can’t promise.
Contrast is a build step
Low-saturation palettes are easy to get wrong: muted text on a muted background quickly drops below what people can comfortably read. So the build checks every pairing that matters against WCAG 2.2 AA and fails if one doesn’t pass:
const failures = checkContrast().filter((r) => !r.pass);
if (failures.length > 0) process.exit(1);
| Pairing | Minimum |
|---|---|
| Body text on any surface | 4.5 : 1 |
| Text on the primary button | 4.5 : 1 |
| Form-control borders | 3 : 1 |
Decorative borders are exempt
Hairlines between sections don’t need 3 : 1 — only the edges of controls you interact with do. Holding every border to 3 : 1 would undo the quiet look we were after.
Three languages from day one
The same Han character is drawn differently in Chinese and Japanese fonts. Setting the right
lang on the page lets the browser pick the right glyphs — and our font stacks are re-declared
per language so that even a short Japanese phrase inside an English page renders correctly.
Good defaults are the ones you never have to think about again.
That’s the whole idea: decide once, encode it as data, and let the build keep us honest.