Skip to content

feat(theme): Export palette and theme colors as Figma variables - #1049

Draft
mdo wants to merge 1 commit into
mainfrom
mdo/theme-figma-variables
Draft

feat(theme): Export palette and theme colors as Figma variables#1049
mdo wants to merge 1 commit into
mainfrom
mdo/theme-figma-variables

Conversation

@mdo

@mdo mdo commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

moonx theme:build now also writes packages/theme/figma/, a Figma-importable copy of the same colors in DTCG token format — so designers can import the Pierre palette into Figma instead of copying hex values by hand.

Two collections, following Figma's variable modes import rules:

  • figma/primitives.json — all 249 palette steps across the 21 scales, one mode. Tokens land as blue/500, neutral/1040, and so on.
  • figma/semantic/*.json — 8 files, one per theme variant (light, dark, both -soft, and the four CVD variants). Figma turns each file into a mode, and every file carries the same 58 role tokens (bg/editor, syntax/keyword, ansi/brightRed, …).

Semantic values alias the primitives through the com.figma.aliasData extension, so editing a primitive in Figma updates every mode that uses it.

Notes for reviewers

Aliases carry a concrete color too. Each semantic token has both a real hex $value and an alias, so a file still imports cleanly if the primitives collection is not present.

Drift from the palette fails the build. Aliases are resolved by matching role hexes back to palette steps, so a role value that is in no palette scale throws. That is deliberate — it catches roles drifting away from the palette. #ffffff (bg.editor and accent.contrastOnAccent in the four light variants) is the one intentional literal, allowlisted in UNALIASED_ROLE_COLORS.

Why palette steps get sorted and serialized by hand. JavaScript hoists integer-like object keys (100) ahead of leading-zero ones (020), so Object.entries(neutral) returns the lightest shades last and JSON.stringify preserves that. Since Figma creates variables in file order, the first draft listed every scale starting at 100 with 020080 stranded at the bottom. Steps are now sorted numerically and groups serialized from Maps to hold that order.

Vibrant variants are excluded. Figma's importer supports only sRGB and HSL, and those themes use color(display-p3 …).

Packaging. figma/** is added to .vscodeignore so the tokens do not ship inside the VSIX; npm was already covered by the files allowlist. The output is committed like themes/*.json, and .oxfmtrc.json ignores it the same way.

Test plan

  • moonx theme:build — writes all 9 files
  • moonx theme:test — 376 pass, including 21 new in test/figma.test.ts
  • moonx theme:typecheck
  • moon run root:format root:lint
  • Deleting figma/ and rebuilding reproduces byte-identical output
  • Mutation-checked the new tests: deleting a token from one mode and corrupting an alias target both fail with actionable messages
  • Needs a real Figma import. The semantic files name Pierre Primitives as their alias target, so cross-collection linking only resolves if the collection is named exactly that (step 1 of the README instructions). Worth confirming aliases resolve and that the 8 files land as 8 modes.

Designers can now import the Pierre colors into Figma instead of
copying hex values by hand. `moonx theme:build` writes `figma/`:
`primitives.json` carries every palette step, and each
`figma/semantic/*.json` is one theme variant that Figma turns into
a mode when the files are imported together.

The generator in `src/createFigmaTokens.ts` emits Design Tokens
Community Group JSON, the format Figma's variable import expects.
Semantic tokens are matched back to palette steps by hex and carry
a `com.figma.aliasData` extension so the two collections link, plus
a concrete color so a file still imports on its own. A role value
that is in no palette scale fails the build, which catches roles
drifting from the palette; `#ffffff` is the one intentional
literal.

Palette steps are sorted numerically and groups are serialized
from Maps because JavaScript hoists integer-like keys ("100")
ahead of leading-zero ones ("020"). Figma creates variables in
file order, so without that each scale would list its lightest
steps last.

The Display P3 vibrant variants are not exported; Figma's
importer supports only sRGB and HSL.
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pierre-docs-diffs Ready Ready Preview Aug 3, 2026 7:06pm
pierre-docs-diffshub Ready Ready Preview Aug 3, 2026 7:06pm
pierre-docs-trees Ready Ready Preview Aug 3, 2026 7:06pm
pierrejs-diff-demo Ready Ready Preview Aug 3, 2026 7:06pm

Request Review

@amadeus

amadeus commented Aug 3, 2026

Copy link
Copy Markdown
Member
CleanShot 2026-08-03 at 12 06 47

should we actually commit the built files?

@mdo

mdo commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

should we actually commit the built files?

Just trying to pad my stats. And no, not likely. I'm playing with this in Figma for now, still not 100% solid yet.

@amadeus

amadeus commented Aug 3, 2026

Copy link
Copy Markdown
Member

word word!

@mdo

mdo commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Okay this is pretty sweet. Works by importing base Pierre Primitives collection, then semantic themes use those tokens on import.

CleanShot 2026-08-03 at 12 32 07@2x CleanShot 2026-08-03 at 12 32 16@2x

@mdo

mdo commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

hi mom

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