@newtone-dev/ui-tokens
v0.12.0
Published
Framework-agnostic design tokens for Newtone: core scales and component-scoped semantic tokens, emitted for every platform.
Downloads
747
Readme
newtone_ui_tokens
Newtone's visual language: colour, typography and spacing as named values, derived from a handful of seeds and emitted for every platform Newtone targets.
Zero dependencies. node build.mjs is the whole toolchain. dist/ is committed, so a
consumer with no Node at all can just take the file it needs.
No network, ever. Which is why families.json — what each typeface can actually do — is
authored and committed rather than fetched. scripts/inspect-family.mjs regenerates an entry
and is run by hand.
The authoritative description of this repo is
~/source/blueprints/newtone/repos/newtone_ui_tokens.md. This README is a summary; if the two disagree, the blueprint wins.
Layers
seed < core < role < semantic- seed — the inputs colour and typography are computed from. Two keys,
colorandfont, each named after the core group it computes. A colour palette is a hue and a saturation and nothing else; saturation is a ratio of the chroma reachable at that hue, so one number means the same thing for a blue as for a yellow. A font role names a family fromfamilies.json, four weights, and optionally axis positions and features. - core — scales.
color.system.accent.25,space.4,font.body.family,breakpoint.compact. Thecolorandfontgroups are computed; there is nocolor.jsonand nofont.json. A source file says what kind of number it holds —"$type": "length"or"ratio", inherited by everything under it — and CSS emitsremoremaccordingly.breakpointis the one group CSS cannot use where it matters: a media query condition cannot read a custom property, so the value is published and the hand-written copies are checked bytools/check-breakpoints.mjsin~/source. - role — colours for things whose area is fixed by what they are.
role.surface.primary,role.divider,role.text.secondary. Eleven of them. - semantic — component tokens.
button.primary.rest.fill. A component never reads a role: contrast that reads as equal depends on the area carrying it, so a button's label and a paragraph cannot share a value.
Above core, a colour is a clamp — color(accent, 13, 23) — not a palette index. It names
a contrast distance, and mode and elevation resolve it.
One exception, and it has to say so out loud. fixed(accent, 2) names an absolute step and
resolves the same in every context. It is for a component that stands in for a photograph, which
has no contrast question to ask; it is legal only in the semantic layer, and a bare
{color.system.accent.2} above core is still a build failure. media-plate.fill is its one use.
A reference points at a strictly lower layer, so cycles are impossible and there is no cycle detection anywhere in the build.
Facets
| Facet | Coordinates | Where it lives |
| --- | --- | --- |
| identity | open — an end user's colour | seed |
| gamut | oklch, srgb | every core colour leaf |
| mode | light, dark | resolved from a clamp |
| theme | default, tinted, emphasis, strong | authored, role + semantic |
| elevation | high, base, deep | resolved from a clamp |
A facet never appears in an emitted token path. theme is written down — one expression per
cell, because a theme replaces the ground and sometimes the palette with it, and no clamp says
that. mode and elevation are never authored: one clamp covers both modes, and all three
elevations, which is what a contrast axis buys.
Eleven roles × four themes is 44 expressions; Button is 39 colour slots × four themes = 156. Everything resolved in a context takes the same elevation offset, so two tokens hold their step distance whatever elevation they are on.
theme is surface intent, not dark mode — strong is an inverted default, and both modes
have all four themes. Since 0.8.0 the inversion is exact: every strong clamp is its default
clamp mirrored, color(p, 23 − max, 23 − min), so strong in light mode is default in dark mode.
Build
node build.mjs # or: npm run buildFive files land in dist/:
| File | For |
| --- | --- |
| newtone.css | the web — custom properties, one block per context |
| newtone.json | canonical; what a runtime parses to re-derive colour |
| newtone.resolved.json | consumers that can do nothing — react-email, static export |
| Newtone.swift | Apple, via newtone_ui_swift |
| Newtone.kt | Android, via newtone_ui_android |
Using it on the web
<link rel="stylesheet" href="dist/newtone.css">
<div data-newtone-surface data-newtone-theme="emphasis">
<button style="background: var(--newtone-button-primary-rest-fill);
color: var(--newtone-button-primary-rest-text)">Primary</button>
</div>Mark a surface with data-newtone-surface, set data-newtone-theme / -elevation on it
and data-newtone-mode anywhere above. Never name a colour. The button above inverts
on an emphasis surface without being told anything.
Changing the look
Edit src/seed/seed.json and rebuild. A hue lives in exactly one place; the ramp, every
role that reads it and every component below all follow.
To re-theme at runtime — a tenant's colours, one page under a collaborator's brand —
set the --newtone-color-* properties on the scope element: fifty-one per palette you are
replacing, and everything downstream follows because it is all references.
example/index.html does exactly this, live, by importing compute/ in the browser — the
same generator the build runs, not a second copy of it.
Studio
npm run studio # then open http://127.0.0.1:4257A local page for editing the clamps in src/semantic/*.json and adding Google families to
families.json. Every save rebuilds dist/, and a value the build refuses is put back with the
build's own message. It commits locally and never pushes. It does not ship in the package.
Example
python3 -m http.server 8000 # then open example/index.htmlSeed sliders, all four themes, elevation nesting, and a scoped identity.
Colour derivation
Specified in ~/source/blueprints/newtone/contracts/color-derivation.md, including test
vectors. compute/ holds the only implementation; build.mjs contains no colour maths at
all. Verified: the neutral and accent palettes re-derive exactly, all fifty-one steps
each, against the contract's tables.
Fourteen palettes of fifty-one steps, from fourteen hues and fourteen saturations. There
is no ramp table anywhere. A step's lightness is found by searching for the OKLCH L
whose CIE L* is 100 - 2 × step, and its chroma is the seed's ratio of the Display P3
ceiling at that lightness — so the chroma curve is the gamut boundary, and a seed cannot
ask for more colour than exists.
Every colour leaf carries a wide-gamut OKLCH value and a precomputed sRGB hex. They are two derivations, not one value encoded twice, and they do not round-trip — one is what a P3 display shows, the other what an sRGB display shows, and they agree wherever both can.
compute/vector.mjs and compute/scale.mjs are a deliberately diffable copy of the
prototype they came from. Do not tidy them; the contract specifies them to the iteration
count.
