Skip to main content

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-hue

264

--accent-hue

60

--neutral-hue

264

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

TokenlightdarkAuthored 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

TokenlightdarkAuthored 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

TokenlightdarkAuthored 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.

PairFloorslightdark
Body text
--ink on --bg
4.5:1
Lc 90
15.97:1 · Lc 99 PASS16.73:1 · Lc 97 PASS
Body text on a card
--ink on --surface
4.5:1
Lc 90
15.30:1 · Lc 96 PASS16.15:1 · Lc 96 PASS
Secondary text
--muted on --bg
4.5:1
Lc 70
6.14:1 · Lc 77 PASS11.71:1 · Lc 74 PASS
Secondary text on a card
--muted on --surface
4.5:1
Lc 70
5.88:1 · Lc 74 PASS11.30:1 · Lc 73 PASS
Large text and icons only
--faint on --bg
3.0:1
Lc 45
3.64:1 · Lc 61 PASS7.09:1 · Lc 49 PASS
Link text
--link on --bg
4.5:1
Lc 60
6.55:1 · Lc 78 PASS11.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 FAIL2.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 PASS8.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 PASS8.64:1 · Lc 66 PASS
Text on a brand fill
--on-primary on --primary
4.5:1
Lc 75
6.51:1 · Lc 85 PASS6.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 FAIL5.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-2

Durations 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.

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.