Design Tokens v2: One Source of Truth for Your Design System

WP Rig’s design token system (SPEC-017) is the single source of truth for colors, typography, spacing, and breakpoints. You describe your design system once in config/tokens.json, and the framework generates CSS custom properties, theme.json editor presets, and — optionally — a Tailwind config. Dark mode, WCAG contrast auditing, and Figma round-trips are built in.

The mental model

Three ideas anchor the system:

  • config/tokens.json is the seed, not a style store. It describes the default color/type/space system and the editor palette only. Component styles stay in CSS — there is no CSS-as-JSON.
  • CSS custom properties are the interface. Components read var(--…). Generated variables live in _tokens.generated.css; your own overrides live in _tokens.custom.css.
  • Pure-CSS authors are first-class. Prefer hand-editing CSS? Skip tokens.json entirely and write _tokens.custom.css — everything still works.

The files, and which ones you edit

PathRoleEdit?
config/tokens.jsonLayered token source (v2)yes
config/tokens.schema.jsonJSON Schema for editor IntelliSenseno
assets/css/src/_tokens.generated.cssGenerated custom properties (gitignored)never — run rig:tokens
assets/css/src/_tokens.custom.cssHand-authored vars/overridesyes
config/theme.custom.jsonHand-authored theme.json fragmentsyes
config/tailwind.custom.jsHand-authored Tailwind extensions (opt-in)yes
tailwind.config.jsGenerated shell (opt-in, gitignored)never

Generated files are gitignored because their content depends on theme.designTokens.colorBinding and the active paradigm. Regenerate with npm run build or npm run rig:tokens — and note that build:css, dev:css, and lint:css regenerate tokens automatically, so a fresh checkout builds without a manual step.

Token taxonomy: three tiers

  • Primitives — raw values with no meaning: primitives.color.brand.500, primitives.space.base.
  • Semantic — roles, each with a light + dark value: semantic.surface, semantic.text, semantic.state.danger.
  • Component (optional) — component-scoped aliases: component.button.bg.

References use {dotted.path} syntax. Primitives may only be referenced by semantic tokens; semantic tokens never reference other semantic tokens; and semantic colors must declare both light and dark values (unless meta.darkMode is "none").

Naming conventions

TierPatternExample
Primitive color--color-{hue}-{step}--color-brand-500
Semantic color--color-{role}--color-surface
Space--space-{step}--space-base
Layout--layout-{slug}--layout-content
Font family / size--font-family-{slug} / --font-size-{slug}--font-family-highlight
Breakpoint--breakpoint-{slug}--breakpoint-tablet
Radius / alpha--radius-{slug} / --alpha-{slug}--alpha-weak

Color math: derive, don’t name

Encode intent, not computation. A token name exists for a role or an axis — never for an intersection. Shades, tints, opacities, and on-colors are derived with CSS math instead of declaring every combination:

/* Alpha from one color token — no --color-accent-50 */
border-color: rgb(from var(--color-accent) r g b / var(--alpha-weak));

/* Tint toward the surface (correct in light AND dark) */
background: color-mix(in oklab, var(--color-accent) 12%, var(--color-surface));

/* Shade for hover/active — derived, not declared */
background: oklch(from var(--color-accent) calc(l - 0.06) c h);

/* Complement without a new token */
color: oklch(from var(--color-accent) l c calc(h + 180));

Dark mode

Every semantic token declares a dark value. rig:tokens generates the @media (prefers-color-scheme: dark) block in _tokens.generated.css — do not hand-write dark overrides for semantic tokens. With colorBinding: "wp-preset" the generated dark block is omitted for preset-bound colors (WordPress presets are single-valued; the Site Editor owns dark). meta.darkMode: "none" disables dark generation entirely — the v1→v2 upgrade sets this so your existing hand-authored dark CSS keeps working.

Configuration

All settings live under theme.designTokens in the config chain:

{
  "theme": {
    "designTokens": {
      "enabled": true,
      "source": "config/tokens.json",
      "colorBinding": "auto",
      "legacyAliases": false,
      "contrast": {
        "level": "AA",
        "onFail": "warn"
      },
      "emit": {
        "tailwind": false
      }
    }
  }
}

colorBinding decides how semantic variables bind: independent keeps WP Rig’s own namespace; wp-preset aliases --wp--preset--color--* so token colors appear as editor presets. auto resolves by paradigm — classic and universal themes get independent, block-based themes get wp-preset. Classic never emits --wp--preset-- references.


Workflows

Token-driven

Edit config/tokens.json, run rig:tokens, commit the CSS you changed — never the generated files:

npm run rig:tokens

Figma-driven

Export Figma Variables to a W3C DTCG JSON (Tokens Studio), import, and round-trip back:

npm run rig:tokens:import -- --from design/figma.tokens.json [--apply]
npm run rig:tokens:export            # default: design/tokens.dtcg.json

Unmapped tokens are reported, never silently dropped.

Site Editor (block-based)

Edit Global Styles in the Site Editor, bake them into config/user-styles.json with rig:bake styles (see Site Editor Bake & Sync), then regenerate. Precedence is tokens < theme.custom.json < user-styles.json, with SSOT keys ($schema, version, settings.viewport, settings.blockVisibility) always owned by tokens.

Tailwind (opt-in)

WP Rig does not ship Tailwind — nothing Tailwind-related is generated by default and no dependency is added. To opt in, install tailwindcss yourself, set theme.designTokens.emit.tailwind: true, and run rig:tokens. That generates config/tailwind.tokens.js and a tailwind.config.js shell, and creates config/tailwind.custom.js for your own extensions (per-key merge, custom wins).


Upgrading a v1 theme to tokens v2

Themes on the old flat tokens.json (top-level colors, typography, spacing, breakpoints) migrate in one reviewed run:

npm run rig:tokens:setup            # dry-run: schema + rename plan
npm run rig:tokens:setup -- --apply  # writes tokens.json v2 + rewrites usages
npm run rig:tokens                   # regenerate
npm run lint:css                     # safety net
  • Schema: flat keys map into primitives + semantic, with meta.darkMode: "none" so existing hand-authored dark CSS keeps working.
  • Names: a collapse-aware codemod rewrites legacy custom properties (--content-width → --layout-content, --global-font-color → --color-text, --mobile-breakpoint → --breakpoint-tablet) across CSS, PHP, and JS — never generated files or Gutenberg color slugs.
  • Backups: every touched file is copied to .rig-backup/<timestamp>/tokens-setup/ before writing.
  • Idempotent: re-running on a v2 tree is a no-op.

Need a staged rollout? Set theme.designTokens.legacyAliases: true to keep emitting --old: var(--new) aliases while you migrate, then flip it off.


Lint rules and gates

  • wprig/no-undefined-custom-properties — flags var(--x) references not in the token inventory.
  • wprig/no-hardcoded-colors — flags literals that exactly match a token value.
  • Build gates: schema validation, reference resolution, WCAG contrast, and the classic preset-ref assertion.

The shipped palette has 6 known WCAG AA diagnostics (the gate runs at warn): link hover, accent, quote-citation (dark), and the success/warning/info state colors. They are tracked for a palette pass; flip contrast.onFail to error once your own palette is clean.

Pair this with Theme Configuration (the theme.designTokens settings in context), Theme Paradigms (how colorBinding resolves per paradigm), and CSS in WP Rig (the Lightning CSS pipeline that consumes these variables).