Design
One token set, five sites
40 custom properties shared verbatim across the portfolio, state-in-url, test-proxy-recorder, react-horizontal-scrolling-menu and x-profile-location — three frameworks and a browser-extension landing between them, with no shared component library and nothing to install.
Every value below is parsed from the token file at build and measured in sRGB, so this page cannot describe a system the sites do not ship.
What a project is allowed to change
Two axes, and no others. A hue angle, and the typefaces the site self-hosts. Identical everywhere else — which is what makes five sites read as one hand without making them look like one template.
--brand-hue264
--accent-hue60
--neutral-hue264
Palette
Authored in OKLCH so a hue change moves every brand colour together and keeps its lightness — the reason one number can rebrand a site. Both columns are rendered from the same file; nothing switches with your system theme.
surfaces
| Token | light | dark | Authored as |
|---|---|---|---|
--bg | #f5f6f7 | #0a0d14 | oklch(0.972 0.0018 var(--neutral-hue)) |
--surface | #f0f1f3 | #0f1219 | oklch(0.9575 0.0027 var(--neutral-hue)) |
--surface-2 | #ebecef | #1c212b | oklch(0.943 0.004 var(--neutral-hue)) |
--line | #dbdee2 | #1e232d | oklch(0.899 0.0068 var(--neutral-hue)) |
--line-strong | #c2c5ca | #333843 | oklch(0.822 0.0084 var(--neutral-hue)) |
text
| Token | light | dark | Authored as |
|---|---|---|---|
--ink | #171b21 | #eceef2 | oklch(0.22 0.0132 var(--neutral-hue)) |
--muted | #595d63 | #c5c9d1 | oklch(0.476 0.012 var(--neutral-hue)) |
--faint | #7d8088 | #999da4 | oklch(0.60 0.012 var(--neutral-hue)) |
brand
| Token | light | dark | Authored as |
|---|---|---|---|
--primary | #1b4ed8 | #1c4fd8 | oklch(0.4882 0.2172 var(--brand-hue)) |
--primary-strong | #0d37ab | #497ef7 | oklch(0.4 0.19 var(--brand-hue)) |
--primary-soft | #e6efff | #15213a | oklch(0.95 0.025 var(--brand-hue)) |
--on-primary | #fbfcfd | #fbfcfd | oklch(0.99 0.002 var(--neutral-hue)) |
--primary-on-soft | #0d37ab | #94c4ff | oklch(0.4 0.19 var(--brand-hue)) |
--accent | #ae5900 | #fe972f | oklch(0.5553 0.1455 var(--accent-hue)) |
--accent-soft | #feeadb | #351e08 | oklch(0.95 0.03 var(--accent-hue)) |
--accent-on-soft | #944900 | #ffb04e | oklch(0.49 0.1324 var(--accent-hue)) |
--link | #174ad3 | #8fcbff | oklch(0.475 0.2172 var(--brand-hue)) |
--focus | #1b4ed8 | #1c4fd8 | var(--primary) |
Contrast contract
Each pair below is measured at build against two models and has to clear both. WCAG 2.1 is the normative one. APCA is the second opinion, and it earns its place: WCAG 2 applies one formula to both polarities and so flatters light-on-dark, which is how a dark --muted once shipped at 6.0:1 — past AA without argument — and Lc 42, under half of body level. Nothing in any suite could see it.
APCA is not a standard, and the WCAG 3 draft neither adopts nor mentions it — its own requirement text still reads @@[contrast measure to be determined]. The Lc floors here are ours, calibrated against what Primer, Carbon, Material and Tailwind ship.
Two rows are meant to be red. A /55 tint of the body colour is why --muted exists at all: it measures under AA and looks perfectly fine, which is the worst combination. And --primary is a fill, held dark enough that --on-primary clears AA on top of it — which leaves it unreadable as text, and is why --link is a separate token. Both rows are here so the failure is visible rather than remembered.
| Pair | Floors | light | dark |
|---|---|---|---|
Body text --ink on --bg | 4.5:1 Lc 90 | 15.97:1 · Lc 99 PASS | 16.73:1 · Lc 97 PASS |
Body text on a card --ink on --surface | 4.5:1 Lc 90 | 15.30:1 · Lc 96 PASS | 16.15:1 · Lc 96 PASS |
Secondary text --muted on --bg | 4.5:1 Lc 70 | 6.14:1 · Lc 77 PASS | 11.71:1 · Lc 74 PASS |
Secondary text on a card --muted on --surface | 4.5:1 Lc 70 | 5.88:1 · Lc 74 PASS | 11.30:1 · Lc 73 PASS |
Large text and icons only --faint on --bg | 3.0:1 Lc 45 | 3.64:1 · Lc 61 PASS | 7.09:1 · Lc 49 PASS |
Link text --link on --bg | 4.5:1 Lc 60 | 6.55:1 · Lc 78 PASS | 11.23:1 · Lc 71 PASS |
--primary as body text — fails on purpose --primary on --bg | 4.5:1 Lc 90 | 6.17:1 · Lc 77 FAIL | 2.92:1 · Lc 20 FAIL |
Brand text on its own tint --primary-on-soft on --primary-soft | 4.5:1 Lc 60 | 8.40:1 · Lc 82 PASS | 8.87:1 · Lc 67 PASS |
Accent text on its own tint --accent-on-soft on --accent-soft | 4.5:1 Lc 60 | 5.59:1 · Lc 71 PASS | 8.64:1 · Lc 66 PASS |
Text on a brand fill --on-primary on --primary | 4.5:1 Lc 75 | 6.51:1 · Lc 85 PASS | 6.45:1 · Lc 85 PASS |
A /55 tint of --ink — fails on purpose --ink / 55.00000000000001% on --bg | 4.5:1 Lc 90 | 3.79:1 · Lc 63 FAIL | 5.54:1 · Lc 39 FAIL |
The table proves the ramp is sound. Proving the pages use it takes a second suite: every visible text node on one page per template, in both themes and with the modal open, is measured in the rendered DOM and held to the weakest floor the contract grants at that size. Lighthouse cannot reach any of that — it audits light mode only, scores nothing without a text node, and never opens a dialog.
It is not decorative. Turned on here it found 122 alpha tints of the body colour, five more in color-mix(), an accent fill used as 13px text, and a heading pair at 1.33:1 — on pages that were scoring 100 for accessibility.
Type scale
A ~1.25 ratio, six steps. The families are the one thing each project sets for itself: this site pairs Outfit with DM Serif Display, the library landings use Schibsted Grotesk, and x-profile-location self-hosts Archivo on a hard LCP budget.
--step-hero Hero Aleksandr Smyshliaev--step-3 Page title Design tokens--step-2 Section Contrast contract--step-1 Subsection Surfaces and text--step-0 Body The quick brown fox jumps.--step--1 Small Captions and metadata.Shape, depth and motion
--radius-s--radius-m--radius-l--shadow-1--shadow-2Durations are tokens too — --dur-fast, --dur-base and --dur-slow — and all three collapse to 1ms under prefers-reduced-motion, once, in the token file. A consuming stylesheet never writes that media query for a transition again.
Who uses it
Distributed by copying the file. No package, no version, nothing to bump — five consumers is well under the point where a registry earns the release ceremony it costs.
264° state-in-url React · TypeScript · Tailwind 300° react-horizontal-scrolling-menu React.js · CI/CD · storybook 15° test-proxy-recorder TypeScript · Playwright · Next.js 262° x-profile-location dark-only — the extension's own mark colour 183°What it deliberately is not
There is no component library. The five projects run on Astro, Next.js, TanStack Start and a Vite landing; a shared React component would need a wrapper per framework and a release for every change, and would buy buttons that match. Tokens are what makes sites look related. Anything that genuinely has to be shared ships as a custom element instead — the for-hire badge is the one thing that has cleared that bar.