Design systems
Three token tiers, the semantic contract every pack satisfies, the thirteen packs and twelve original templates, and how to build your own.
A UI document never contains a colour, a font size or a radius. Every visual value comes from a design-system pack: a set of design tokens in the W3C DTCG 2025.10 format. Swap the pack and the same document renders in a different design system, with no change to the JSON.
Three token tiers#
| Tier | What it holds | Example |
|---|---|---|
| Primitive (reference) | Raw palettes, type families, spacing steps | md.ref.palette.primary.40, carbon.color.blue.60 |
| System | The design system's own named roles, often per mode | md.sys.color.on-surface, carbon.theme.text-primary, antd.token.colorText |
| Semantic | The Polyxd contract: the only tier renderers and documents see | color.text.default, type.body.default, size.target.min |
Each pack keeps its design system's own names in the primitive and system tiers, so every semantic token can be traced back to its source. Semantic tokens are {alias} references to system tokens wherever an equivalent exists.
The semantic contract#
packages/spec/tokens/semantic-contract.json (contract version 0.1.0) lists what every pack must provide, in every mode it declares:
- 87 tokens: colour (43: surfaces, text, borders, actions, selection, status, data), type (8), space (10), motion (7), radius (5), opacity (4), size (3), border (2), focus (2), shadow (2) and measure (1).
- 34 contrast pairs: 18 text pairs at WCAG 2.2 1.4.3 (4.5:1) and 16 non-text pairs at WCAG 2.2 1.4.11 (3:1).
- 4 constraints:
| Token | Constraint | Why |
|---|---|---|
type.body.default |
font size at least 16px | Readable body text |
size.target.min |
at least 24px | WCAG 2.2 2.5.8 target size (minimum) |
measure.max |
45–75 | Comfortable line length |
opacity.state.disabled |
0.3–0.6 | Disabled content stays perceivable but clearly inactive |
The contract checker resolves aliases, checks each token's type, measures every contrast pair (compositing translucent colours before measuring), and checks the constraints.
The packs#
| Pack | Modes | Built from |
|---|---|---|
@polyxd/ds-material3 |
light, dark | @material/material-color-utilities 0.4.0 (seed #6750A4, tonal spot) and material-web v0_192 token sources |
@polyxd/ds-carbon |
light (Carbon White), dark (Carbon Gray 100) | @carbon/colors, themes, layout, type, motion and charts, at pinned versions |
@polyxd/ds-antd |
light (defaultAlgorithm), dark (darkAlgorithm) |
antd 6.6.4 and @ant-design/colors 8.0.1 |
@polyxd/ds-fluent |
light (webLightTheme), dark (webDarkTheme) |
@fluentui/tokens 1.0.0-alpha.24 |
@polyxd/ds-shadcn |
light, dark | shadcn/ui's default theme variables and Tailwind CSS 4.3.3's scales |
@polyxd/ds-bootstrap |
light, dark ([data-bs-theme]) |
bootstrap 5.3.8's CSS custom properties |
@polyxd/ds-mantine |
light, dark ([data-mantine-color-scheme]) |
@mantine/core 8.4.2's stylesheet |
@polyxd/ds-radix |
light, dark | @radix-ui/themes 3.3.0 at its defaults: indigo accent, medium radius, 100% scaling |
@polyxd/ds-polaris |
light, dark (.p-theme-dark) |
@shopify/polaris-tokens 9.4.2's stylesheet |
@polyxd/ds-primer |
light, dark | @primer/primitives 11.10.0's base scales, functional scales and theme files |
@polyxd/ds-spectrum |
light, dark | @adobe/spectrum-tokens 15.4.1's variables.json, desktop scale |
@polyxd/ds-govuk |
light (GOV.UK has one theme) | govuk-frontend 6.5.1's Sass settings |
@polyxd/ds-chakra |
light, dark | @chakra-ui/react 3.37.0, via its own getTokenCss() emitter |
Each pack is generated by a script from vendored, version-pinned sources, gives the same output on every run, and records provenance in its manifest. None of the values are scraped from design-system websites.
A pack's manifest also records its logo, and the pack ships it: "logo": { "file": "logo.svg", "source": "<the owner's URL>", "guidelines": "<their brand rules>" }. It is the system's official file, unaltered, from the owner's own site or repository, and Polyxd shows it only beside the system's name, on a white tile, to say which system a pack is modelled on. Where the owner doesn't allow that, the manifest has words instead of a file ("text", with a note saying why) and the pack is named without a logo: GOV.UK, whose crown and logotype are protected, and Material 3, Carbon, Polaris, Fluent 2, Spectrum 2 and Primer, whose owners allow their logos only with permission. Six packs have their logo today: shadcn/ui, Ant Design, Bootstrap, Chakra UI, Mantine and Radix.
Templates#
Alongside the 13 design systems there are twelve original templates: packs that reproduce nobody's brand, each with one clear character, each passing the same contract check in light and dark. They exist to be copied and changed. A team without a design system starts from the nearest one; a team with one uses them to see what a role is for before mapping its own tokens. Their manifests carry "template": true and provenance that says so. Each has a small mark, logo.svg, that Polyxd draws from the template's own tokens (its ground, border, radius, text, primary and accent) with node packages/ds-kit/scripts/pack-logos.ts; a test fails if one no longer matches its tokens.
| Template | Character | Type |
|---|---|---|
@polyxd/ds-sketch |
Hand-drawn: paper and ink, pencil-grey borders, no shadows, uneven corners, wavy underlines, cards that lean a third of a degree | Caveat, Patrick Hand, Nunito |
@polyxd/ds-wireframe |
The second sketch look, low-fidelity: greyscale, dashed borders, placeholder blue for anything you can act on, hatched media, mono labels | System sans, JetBrains Mono |
@polyxd/ds-editorial |
Serif display, warm paper, hairline rules, a 72-character measure, oxblood accent; magazines and long reads | Fraunces |
@polyxd/ds-brutalist |
Two-pixel black borders, flat yellow and black, hard 4px shadows, no radius anywhere, mono labels | Space Grotesk, Arial, JetBrains Mono |
@polyxd/ds-glass |
Translucent surfaces over a cool ground, an edge highlight, wide soft shadows, 20px corners | System sans |
@polyxd/ds-terminal |
Dark by default, monospace in every role, phosphor green and amber, 2px radii, dense | JetBrains Mono |
@polyxd/ds-pastel |
Mint, lavender and peach, pill buttons, 16px cards, a friendly rounded sans; consumer apps | Nunito |
@polyxd/ds-civic |
Plain and high-contrast: 18px body, one blue, 48px targets, a thick focus ring; public services | System sans |
@polyxd/ds-finance |
Navy and gold, serif titles, tabular figures, 4px radii, dense tables; banking and trading | Georgia, system sans |
@polyxd/ds-health |
Calm teal on cream, extra spacing, 48px targets, warm feedback colours; care and wellbeing | System sans |
@polyxd/ds-neon |
Dark by default, magenta primary, cyan links, shadows that glow; entertainment and gaming | Space Grotesk |
@polyxd/ds-mono |
One hue in every role, written as oklch(L C 275): change the hue once and the whole system follows |
System sans |
The Google Fonts they name are OFL and are not shipped; each falls back to a stated system stack and still passes every check without them.
The two sketch looks need one thing tokens can't say. A wobbly corner, a wavy underline or a dashed border isn't a value, so a manifest may name an extras stylesheet ("extras": "tokens/extras.css") that the theme compiler appends verbatim to the pack's theme CSS. It is held to two rules, checked at build: every selector is scoped by [data-pxd-theme="<name>"], and it names no colour of its own — only var(--pxd-…) tokens — so recolouring the tokens recolours the extras. Sketch and wireframe are the only packs that use it, and a test keeps it that way.
Each template's README says what it is for and how to make it yours: edit its palette (every role points at a named colour), or run npx polyxd pack on your own tokens and keep the template beside you as a reference.
Layout a token can't express#
One thing kept not fitting: a dialog's footer. Carbon's runs the full width of the dialog in equal halves; Material's, Ant's and everyone else's sits inline at the trailing edge. That is a layout decision, not a value, and copying Carbon's into the renderer as a special case would have put a design system's name in the renderer.
So a manifest may set layout, a small set of variables the renderer defines and documents — currently action-gap, action-item and action-bleed. The renderer's stylesheet reads them with its own defaults, so a pack that says nothing keeps the inline footer, and a pack may set these variables but may never add a rule. Carbon is the only pack that sets any today:
"layout": {
"action-gap": "1px",
"action-item": "1 1 0",
"action-bleed": "var(--pxd-space-stack-default) calc(-1 * var(--pxd-space-inset-comfortable)) calc(-1 * var(--pxd-space-inset-comfortable))"
}
What building the packs found#
Mapping thirteen real design systems onto one contract surfaced real accessibility gaps. The packs fix them by moving to the nearest passing value from the same design system, and leave the original tokens in place.
- Material 3 passes every contrast pair with its raw roles in both modes. No tones were adjusted.
- Ant Design's defaults fail WCAG in several places. White on the primary blue
#1677ffis 4.10:1, below 4.5:1 for normal text. Warning text on the warning background is 1.83:1. The default input border#d9d9d9is 1.41:1 against the 3:1 non-text minimum. The pack moves each failing role to the nearest passing step of the same Ant palette; for example, the primary background becomes step 7#0958d9(6.16:1). - Carbon's light warning colour (
$support-warning, yellow 30) is 1.68:1 on white. The pack uses Carbon's own$status-yellow-outline(yellow 60, 4.99:1) instead. - Disabled opacity is below the contract's 0.3 minimum in both Carbon (0.25, raised to 0.3) and Ant (0.25, raised to 0.45, the next step of Ant's text-alpha ladder).
- Body text is 16px, not 14px. Both Carbon and Ant default to 14px body text. The contract requires
type.body.defaultof at least 16px, so both packs map it to their 16px style (body-02in Carbon,fontSizeLGin Ant) and keep 14px fortype.body.smalland labels. - Fluent's default stroke (
colorNeutralStroke1) is 1.46:1 in light and 2.87:1 in dark. Fluent publishes the answer itself:colorNeutralStrokeAccessible. Its dark brand fill is 2.48:1 on the dark canvas, so the pack usescolorBrandBackgroundStatic(3.06:1, white text on it at 5.38:1). - shadcn's focus ring is 2.59:1 on white, and a focus indicator that faint is hard to find by keyboard. Its muted text is 4.34:1 on its own muted fill. Both move to the nearest step of Tailwind's neutral ramp that passes; dark mode keeps shadcn's own values.
- shadcn defines no success, warning or info colour — only
destructive— and its chart ramp is five steps of one blue. Status colours and six distinguishable chart hues come from Tailwind's palette, marked as Polyxd additions. - Translucent colours (Ant's neutrals are alpha colours, shadcn's dark borders are
oklch(1 0 0 / 10%)) are composited on the real background before contrast is measured. - OKLCH. Tailwind v4 and shadcn publish colours in OKLCH, so the contract's colour maths converts OKLCH to sRGB before measuring: WCAG contrast is defined on sRGB luminance. It also reads short hex (
#fff, Bootstrap's habit) and plain keywords (white, which Radix writes). - Bootstrap's bright hues can't carry a 3:1 accent in light mode — cyan is 1.35:1 on white, yellow 1.23:1 — and Bootstrap already solves this with
*-text-emphasisvariables that darken in light and lighten in dark. Every status emphasis and chart colour uses those. - Mantine's filled buttons put white on step 6 of a hue, which is 3.56:1 — below 4.5:1 for their own label. The pack measures each role against the surface it sits on and moves along Mantine's own ramp until it passes; where nothing passes (green and yellow tints), status text falls back to the body colour and the token says so.
- Polaris's dark theme is partial. As of 9.4.2 it overrides forty variables and leaves every status colour at its light value — a light green success chip on a dark page is what Polaris itself renders. The pack keeps those chips (they are internally consistent) and moves only what the contract measures against the page: status accents, the destructive button, the link colour and the focus ring, each along Polaris's own status family, sorted by measured luminance because the order of its role names is not the order of its colours.
- Primer needed no colour moved at all — GitHub tunes its semantic pairs — but its dark muted fills are translucent (
bgColor-danger-mutedis#f851491a), so the pack composites each one over the page and stores the colour a reader actually sees. - Spectrum numbers its ramps by contrast, not by lightness.
gray-25is white in light and near-black in dark;gray-800is body text in both. A role that needs more contrast moves up the numbers in either scheme, which makes Spectrum the one pack whose search direction doesn't flip. The same logic sets a button's fill: its label isgray-25, so the fill is measured against that rather than against literal white. - GOV.UK has no tokens, no dark mode, no shadows, no radii and no motion. Its palette and scales live in Sass maps, which the pack reads; the rest is not missing data but a design position, and the pack records it as one — zero radii, a transparent shadow, zero durations. Its focus state is the interesting case: a yellow block with a black bar under it, where the yellow alone is 1.35:1 and the bar carries the contrast. A single-colour ring can only be one of the two, so it takes the black.
- Chakra's focus ring is a 1.48:1 hairline — its neutral
border-emphasized— and a focus ring is the one thing a keyboard user has to find. It moves up Chakra's own gray ramp until it is a 3:1 graphic, as do its status texts, which sit 4.4:1 on their own tints. - A unitless number is not a length. Several packs write line heights as ratios (
1.5), and typing one as a DTCG dimension compiles toline-height: 1.5px, which collapses every line box in the pack. The kit now types a unitless value as a number whatever the mapping says, and the theme compiler keeps a dimension's unit so a pack whose line heights are real lengths (Polaris, Radix, Fluent) still works. - Radix Themes' steps carry meaning (9 is a solid fill, 11 accessible text, 12 high contrast), so little needed moving. The exception: no solid red step carries white text at 4.5:1 — step 9 is 3.91:1, and in dark mode the ramp gets lighter, not darker. Step 11 flips with the mode, so pairing it with step 1 passes both ways.
- Wide-gamut duplicates. Radix publishes every scale twice, sRGB and display-p3 inside
@supports; the packs read the sRGB ones.
Each pack's README lists every mapping and adjustment with before and after ratios.
Theme CSS#
@polyxd/react compiles each pack into one CSS file, themes/<pack>.css. Only semantic contract tokens are emitted; renderers never see primitives.
[data-pxd-theme="material3"]:not([data-pxd-mode]),
[data-pxd-theme="material3"][data-pxd-mode="light"] {
--pxd-color-surface-default: #fdf7ff;
--pxd-color-text-default: #1d1b20;
--pxd-color-action-primary-background: #65558f;
/* … every contract token … */
/* shadcn/ui variable names, so shadcn components follow this pack too */
--background: var(--pxd-color-surface-default);
--primary: var(--pxd-color-action-primary-background);
--radius: var(--pxd-radius-default);
color-scheme: light;
}
Token names become variables by replacing dots with dashes and adding --pxd-: color.text.default is --pxd-color-text-default. Typography tokens expand into -family, -size, -weight, -line-height and -letter-spacing.
Each theme also sets shadcn/ui's variable names (--background, --foreground, --card, --popover, --primary, --secondary, --muted, --accent, --destructive, --border, --input, --ring, --radius, --chart-1 to --chart-5), so an existing shadcn app picks up the same pack.
The PolyxdSurface theme and mode props set data-pxd-theme and data-pxd-mode on the surface element.
Make your own pack#
A pack is a manifest plus DTCG token files.
1. Write the manifest (manifest.json, validated by schema/design-system.schema.json). Files listed for a mode are merged in order, later files overriding earlier ones.
{
"$schema": "../spec/schema/design-system.schema.json",
"name": "acme",
"displayName": "Acme",
"version": "0.1.0",
"contractVersion": "0.1.0",
"license": "Apache-2.0",
"modes": {
"light": ["tokens/primitive.json", "tokens/system.json", "tokens/system.light.json", "tokens/semantic.json"],
"dark": ["tokens/primitive.json", "tokens/system.json", "tokens/system.dark.json", "tokens/semantic.json"]
},
"defaultMode": "light",
"provenance": [{ "source": "Acme brand tokens", "version": "2026.1" }]
}
2. Write DTCG token files. Keep your own names in the primitive and system tiers, and alias them from a semantic file that defines all 86 contract tokens. A $type on a group is inherited by its tokens.
{
"color": {
"$type": "color",
"text": {
"default": { "$value": "{acme.color.ink}" },
"muted": { "$value": "{acme.color.ink-soft}" }
}
}
}
3. Check it against the contract.
npm run check-ds -w @polyxd/spec -- ../ds-acme/manifest.json
[light] color.text.muted: contrast 3.92:1 against color.surface.default is below 4.5:1 (WCAG 2.2 1.4.3)
1 issue(s)
It exits with 0 and prints "Design system satisfies the semantic token contract." when every mode passes. From code, use checkDesignSystem(manifestPath) from @polyxd/spec.
4. Compile it to CSS. Put the pack in packages/ds-<name>/ and run npm run build:themes -w @polyxd/react, or pass manifest paths to scripts/build-themes.ts directly.
Studio imports a design system as it is: an npm package, a .tgz, a Tokens Studio file, a W3C DTCG file or CSS custom properties. An importer for Figma variables is planned.
Polyxd is an early preview. Found something unclear? It will get better with your feedback.