Skip to content

spike(Design Tokens) - #3406

Draft
dreamwasp wants to merge 2 commits into
mainfrom
cass-dtcg-tokens-poc
Draft

spike(Design Tokens)#3406
dreamwasp wants to merge 2 commits into
mainfrom
cass-dtcg-tokens-poc

Conversation

@dreamwasp

@dreamwasp dreamwasp commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Overview

Spike, not for merge. Closes go/no-go criterion #3 for the 1.0 reboot: stand up
a DTCG → Style Dictionary pipeline emitting CSS variables + TS types, and verify
parity with today's typed tokens.

Passed. Full writeup in spikes/dtcg-tokens-poc/README.md.

Why it matters more than "tokens in a new format"

The reboot recommendation proposes tokens move to an engine-neutral source so they
survive an engine change. But every earlier spike had Panda generating the tokens,
which coupled tokens to Panda — the opposite of the stated goal. The only DTCG reference
anywhere was a comment saying "real Gamut would source these from the same DTCG /…",
explicitly deferred.

This closes that gap. From the real Core theme:

  • 68 palette colours byte-identical to what Gamut emits today
  • 32 semantic aliases × 2 modes as var() references
  • 137 token names matching keyof theme[scale] exactly
  • 139 tokens identical across CSS / TS / a generated Panda preset

That last one is the point: Panda becomes a consumer of the tokens rather than where
they live
, so "engine-neutral" is now demonstrated rather than aspirational.

The constraint worth knowing about

DTCG has no concept of modes. The 2025.10 spec gives a token exactly one $value;
Gamut has 5 themes × 2 modes. So each mode is a separate token file merged at build
time — correct output, but the mode dimension lives in build configuration rather than
in the tokens
. A consumer handed the JSON alone cannot discover that modes exist.

Fine while Gamut owns the build. Not fine if tokens are handed to Figma or a partner
expecting modes to travel with them.
That is now a deliberate decision rather than an
unknown.

Two values aren't expressible at allborders.1 / borders.2, CSS shorthands
embedding a var() reference to a mode-dependent semantic colour. Recorded explicitly
in tokens/.unexpressible.json rather than quietly dropped, and borders stays
hand-maintained outside DTCG (2 tokens; composite + modes not worth it).

The verification is designed to fail loudly

verify.mjs uses an EXPECTED_DEVIATIONS allowlist, so a new deviation still fails
the run while known ones stay documented. Of the reboot's spikes this is the assertion
design most worth copying — most of the others report numbers without failing.

94feea34d makes the export idempotent, so re-running produces no diff.

PR Checklist

  • Related to designs: n/a — token pipeline
  • Related to JIRA ticket: GMT-1715
  • Version plan added/updated (or not needed) — not needed, spike
  • I have run this code to verify it works
  • This PR includes unit tests for the code change — verify.mjs is the assertion; it
    exits non-zero on any unexpected deviation
  • This PR includes testing instructions
  • The alpha package of this PR is passing end-to-end tests in all relevant
    Codecademy repositories — no; nothing consumes these tokens yet

Testing Instructions

yarn workspace dtcg-tokens-poc all     # export → build → verify

Expect: 68 colours byte-identical, 137 names matching keyof theme[scale], 139 tokens
agreeing across all three consumers, and a non-zero exit if any unlisted deviation
appears. Re-run it twice — the export is idempotent, so the second run should produce no
diff.

Note this adds spikes/* to the root workspaces.

Known follow-ups

  • Only Core is exported. The other four themes still need doing, and Core's palette
    is not a superset
    , so this is not a trivial repeat.
  • The generated Panda preset is not yet wired into emotion-to-gamut-poc's
    panda.config.ts — doing so would close the loop and prove the whole split end to end.
  • Nothing consumes these tokens in a real build yet.

PR Links and Envs

Repository PR Link
Mono none — no consumer integration attempted
Monolith none — no consumer integration attempted

Related reboot spikes: #3402 (styling proof of concept) · #3405 (Panda under the hood) ·
#3407 (tsdown build).

dreamwasp and others added 2 commits August 10, 2026 09:19
Go/no-go criterion #3 from the reboot recommendation §5, and the only one of
the four that had never been spiked. It matters because §3 recommends tokens
move to an engine-neutral pipeline while every other spike has Panda
generating tokens directly — the opposite of the stated goal.

It works. From the real Core theme, exported to DTCG and built with Style
Dictionary:

  palette colours      68 byte-identical to what Gamut emits today
  semantic aliases     32 per mode x 2 modes, as var() references
  token names         137 match keyof theme[scale] exactly
  engine-neutrality   139 tokens identical across CSS / TS / Panda preset

The Panda preset is generated FROM the tokens rather than being where they
live, so swapping the styling engine no longer touches the token pipeline.
Aliases emit var(--color-x) rather than inlined hex, so colour mode still
works by variable reassignment and stays a x1 multiplier.

Findings, all in the README:

- DTCG has no concept of modes. Each mode is a separate token file merged at
  build time, so the mode dimension lives in build config rather than in the
  tokens — fine while Gamut owns the build, not fine if tokens are handed to a
  third party expecting modes to travel with them.
- spacing[0] can't round-trip: DTCG dimensions require a unit, so bare 0
  returns as "0px". keyof is unaffected, so type safety holds; only the value
  type shifts. Recorded as an explicit accepted deviation so new ones fail.
- Style Dictionary's TS formats can't express Gamut's token names (navy-800,
  0, 400 aren't valid JS identifiers). name/camel would rename them, which
  parity forbids, so a custom format emits a quoted-key as-const object.
- borders isn't expressible: a CSS shorthand embedding a var() ref to a
  mode-dependent semantic colour. Decomposing it is the real fix.

style-dictionary pinned to 5.5.0 rather than ^5.5.1 to respect the repo's
npmMinimalAgeGate of 7d. Adds spikes/* to root workspaces, which main lacks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
.unexpressible.json was written without a trailing newline, so every run left
the working tree dirty against the lint-formatted committed copy. Also ignore
the drafted PR body.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dreamwasp dreamwasp changed the title Cass dtcg tokens poc spike(Design Tokens) Aug 11, 2026
@nx-cloud

nx-cloud Bot commented Aug 11, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 94feea3


☁️ Nx Cloud last updated this comment at 2026-08-11 13:50:37 UTC

@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

⚠️ JUnit XML file not found

The CLI was unable to find any JUnit XML files to upload.
For more help, visit our troubleshooting guide.

@github-actions

Copy link
Copy Markdown
Contributor

@codecademydev

Copy link
Copy Markdown
Collaborator

📬 Published Alpha Packages:

Package Version npm Diff
@codecademy/gamut 72.5.2-alpha.780104.0 npm diff
@codecademy/gamut-icons 9.57.12-alpha.780104.0 npm diff
@codecademy/gamut-illustrations 0.58.17-alpha.780104.0 npm diff
@codecademy/gamut-kit 3.0.15-alpha.780104.0 npm diff
@codecademy/gamut-patterns 0.10.36-alpha.780104.0 npm diff
@codecademy/gamut-styles 20.1.1-alpha.780104.0 npm diff
@codecademy/gamut-tests 6.0.7-alpha.780104.0 npm diff
@codecademy/variance 0.26.2-alpha.780104.0 npm diff
eslint-plugin-gamut 2.4.4-alpha.780104.0 npm diff

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants