@seekalfred/tokens
v0.5.1
Published
Design tokens: three tiers, DTCG source, compiled to CSS custom properties and the single Tailwind @theme block.
Readme
@seekalfred/tokens
Design tokens in three tiers, authored as DTCG JSON and compiled to CSS custom
properties plus the single Tailwind @theme block.
You do not install this directly — @seekalfred/styles imports it.
The tiers
| Tier | Prefix | Who may reference it |
| --- | --- | --- |
| 1. Primitive | --alfred-* | Tier 2 only. Never a component. |
| 2. Semantic | --color-*, --shadow-* | Components, themes |
| 2b. Scale | --text-*, --radius-*, --leading-*, … | Components |
| Density | --density-* | Components |
Tier 1 is unreachable from a component by construction, not by review: it
sits outside every Tailwind namespace, so bg-orange-500 cannot compile. The
@theme block also opens with --color-*: initial, which deletes Tailwind's
default palette so bg-blue-600 cannot compile either.
Every value is verbatim from the Alfred brand system. Do not re-pick them by eye, and port by role rather than by matching a variable name.
Scope axes
Two independent attributes, composable:
<html data-theme="dark" data-density="compact" dir="rtl"></html>data-theme—light(default) ·dark. Overrides the semantic tier only, which is what keeps a theme a short override instead of a fork. Brand orange, periwinkle and the destructive fill are theme-invariant.data-density—comfortable(default) ·compact·spacious. Changes space, never type. Every scope declares all 21 names, because custom properties inherit and a missing one would let a nested region silently take its parent's metrics.density-contract.test.tsenforces it.
Both axes rely on theme.css being imported inside layer(alfred.tokens).
Unlayered @theme output beats every layered rule regardless of specificity,
which silently broke dark mode entirely until it was fixed.
Contrast is a contract, not a review note
tokens/contrast-pairs.json lists every foreground/background pair a component
actually renders, with its minimum ratio. A node test computes each ratio for
every theme and fails the build.
This catches what axe cannot: axe checks text only, on whatever pairs a story happens to render, in whichever theme the run used — and it has no rule at all for borders, icons or focus rings (WCAG 1.4.11).
Two consequences worth internalising:
- Fills are named as pairs —
--color-primary-foregroundbeside--color-primary— so the contract is expressible. - A fill is not automatically usable as text.
--color-primaryis 2.44:1 on the page, under even the 3:1 non-text floor, so the link label and the outline border use--color-primary-strong.
Fonts: tokens ship, files do not
The type tokens name Satoshi first and fall back to ui-sans-serif,
system-ui, so an unconfigured consumer gets the system stack rather than a
broken page.
The font files are deliberately not shipped. Satoshi is licensed under the ITF Free Font License, which restricts redistribution of the files — obtain it from Fontshare yourself. Bricolage Grotesque, Inter and JetBrains Mono are OFL 1.1 and may be redistributed with their license text.
There is no importable fonts.css, on purpose: CSS url() cannot read a custom
property, so a shipped stylesheet could only guess your asset path and 404.
Host the files yourself and paste this:
/* Prefer the VARIABLE Satoshi (300–900 axis). The five static weights ship
300/400/500/700/900 with NO 600 — and --font-weight-semibold is 600, which
drives every heading, so the static set silently synthesizes up to Bold. */
@font-face {
font-family: "Satoshi";
src: url("/fonts/Satoshi-Variable.woff2") format("woff2-variations");
font-weight: 300 900;
font-style: normal;
font-display: swap;
}--font-mono (JetBrains Mono) and --font-accent (Bricolage Grotesque) follow
the same pattern and are optional — both degrade to sensible system stacks.
Gotcha: tailwind-merge and line-height
cn() runs tailwind-merge, which assumes a text-* class also sets
line-height — true in Tailwind v3, not here. So a leading-* that appears
before a text-* class in a merged class list is silently dropped, with no
error and no visual clue.
If a component needs an explicit line-height, put leading-* after its text-*
class, or in the same variant string. Controls with a fixed height and flex
centring do not need one at all.
Editing
tokens/*.json is the source of truth. dist/ is generated — never hand-edit
it. After any change:
node build-tokens.mjs # or: pnpm --filter @seekalfred/tokens build
pnpm --filter @seekalfred/tokens testAdding a token is a patch. Renaming or removing one is breaking — while
0.x that means a minor, and it needs a changeset.
