Skip to content
Transtyle v0.1.0-alpha.5
ok: build 10:17 UTC31 pages main · 7ed21c5

Alpha — breaking changes ship without a deprecation cycle. What is built vs. planned

You already have a design system

This is the primary use case. Your design system exists in Figma, in Sass variables or in a brand book, with its own names and its own logic. You don’t rename anything. You express your system in its own vocabulary, then bind Transtyle’s catalog to it with one-line aliases. Five steps.

In Figma-token terms

If you use primitive → alias → component token layering (Tokens Studio, Figma variables collections), your primitives go in option.* unchanged. A Tokens Studio export loads as it is, see below. Your alias layer becomes custom semantic tokens under your names. The binding file below is a second, tiny alias layer that only the build pipeline knows about. Your designers never see it.

1. Dump your raw values into option.*, verbatim#

Your palette, your names, your structure. This tier is private vocabulary, and nothing downstream depends on its shape:

{
  "option": {
    "color": {
      "$type": "color",
      "flame": { "$value": "#e8590c" },
      "coal": { "900": { "$value": "#1a1614" }, "600": { "$value": "#57504b" } },
      "sand": { "50": { "$value": "#faf6f1" }, "200": { "$value": "#eadfd3" } }
    }
  }
}

Paste whatever your stylesheets already contain. Hex (including 4/8-digit alpha), rgb()/hsl() in either the modern or legacy comma form, hwb(), lab(), lch(), oklab(), color(display-p3 …), and CSS named colors like red or purple all parse. A DTCG export from a design tool pastes as is too: Transtyle reads its color objects ({ "colorSpace": "srgb", "components": […], "hex": "#…" }) in all fourteen DTCG color spaces. OKLCH is canonical internally and conversion is automatic, so you never retype a value to satisfy the compiler.

No palette? Synthesize one, the names are throwaway#

The step above assumes you have a primitive tier. Plenty of real products never built one: colors are literals written at the point of use, and the same value shows up in a dozen places under a dozen different names. If that’s you, you’re not doing it wrong. You just have one extra job before the interesting part.

Collect the distinct values and give them any names at all. That’s the whole task. The option tier is private vocabulary that nothing downstream binds to, so the names are scaffolding, not decisions. You’ll never defend them in a design review:

{
  "option": {
    "color": {
      "$type": "color",
      "gray-333": { "$value": "#333333" },
      "gray-ddd": { "$value": "#dddddd" },
      "blue-link": { "$value": "#3366cc" }
    }
  }
}

Naming by value (gray-333) or by hue-and-lightness (blue-600) both work and both take minutes. Resist the urge to name them semantically here. “brand-primary” belongs in step 2, where it can be argued about, not in the tier you’ll regenerate whenever your palette changes.

The duplication you find is a result, not an obstacle

In a real run against a product with no tokens, 89 CSS custom properties collapsed to 29 distinct values. One gray appeared under 13 different names (--body-color, --title-color, --panel-color, …), with nothing recording that they were the same decision. Discovering that is the first thing adoption pays you, before a single theme is compiled. Expect the ratio; don’t be alarmed by it.

2. Express your existing semantics with your names#

If your system already has meaning-level names (“flame is our action color”, “sand is our canvas”), write them as custom semantic tokens. They’re first-class: carried, resolved per mode, provenance-tracked, and accounted for on every target. The build prints how many reach it through your bindings, and report.json lists the rest as dropped:

{
  "semantic": {
    "color": {
      "$type": "color",
      "brand-action": { "$value": "{option.color.flame}" },
      "canvas": { "$value": "{option.color.sand.50}" }
    }
  }
}

If your names are component-scoped rather than meaning-scoped (--button-primary-background, --table-th-background, the usual shape when a product grew its CSS organically), keep them exactly as they are. They’re still your system’s real vocabulary, and step 3 binds by meaning regardless of how a name is spelled. Renaming them into meaning-words here is the one thing not to do: it invents a semantic layer your team never agreed on, in a file they’d have to maintain.

Mode variants go in separate pure-DTCG files. That’s the recommended layout: every file stays readable by your existing tooling (Figma, Tokens Studio, Style Dictionary), with the mode assignment in the config, not the tokens:

// tokens/dark.tokens.json — plain DTCG, only the tokens that vary
{ "semantic": { "color": { "canvas": { "$value": "{option.color.coal.900}" } } } }
// transtyle.config.json
"tokens": [
  "tokens/*.tokens.json",
  { "files": "tokens/dark.tokens.json", "mode": { "color-scheme": "dark" } }
]

(The inline $extensions form exists too, for small hand-edited systems.)

3. Bind the catalog: the translation layer#

One small file of aliases connects the Transtyle language to yours. This file is knowledge about your system, not part of it. Keep it separate and own it in the platform team:

{
  "semantic": {
    "color": {
      "$type": "color",
      "primary": { "solid": { "$value": "{semantic.color.brand-action}" } },
      "elevation": { "0": { "surface": { "$value": "{semantic.color.canvas}" } } },
      "text": { "base": { "$value": "{option.color.coal.900}" } },
      "border": { "base": { "$value": "{option.color.sand.200}" } }
    }
  }
}

Don’t translate everything on day one. Bind what you’re sure of; the coverage report will show you what derivation guessed for the rest.

Start from a suggestion. npx transtyle bind --suggest > tokens/bindings.suggested.tokens.json drafts this file from your own names and colors. For each slot nothing binds yet, it gives the token that fills it, with a confidence (high when the name and the value agree) and the reason in its $description. It reads a name by its structure, not by similarity (text-secondary is a text rung, not the secondary role). A slot it can’t settle stays out of the file, listed as contested. It’s a draft: read every line, delete what you disagree with, then add the file to tokens. --rules prints the same proposals as bindings rules for the config (CLI reference).

A binding can also point at another catalog slot when two meanings share a colour in your system. "secondary": { "solid": { "$value": "{semantic.color.info.solid}" } } makes secondary the info blue and still derives secondary’s own grid (hover, tint, on-colours), whatever the order of the roles. The one limit is a slot derived after the role grids. Bound to ring, link.* or palette.categorical.*, a role keeps the colour but gets no grid, and TST1205 says so.

4. Build, and read the report#

npx transtyle build

Open report.json (or read the · derived comments in the output). Every variable is classified: what came from your system, what was derived from it, what was approximated. Derived values aren’t wrong. They’re proposals computed from your brand. The hover states and tinted backgrounds will already be coherent with your colors.

Then read what is left on your side: npx transtyle check ends with an adoption block that counts your own semantic tokens and lists the ones no catalog slot reads. Those reach css-variables verbatim and no other target. Each comes with hints: the slots already set to the same value, or the slot whose name it shadows. GOV.UK’s govuk.focus-text is “same value as text.base”, the black drawn on the yellow focus state, which the catalog has no foreground slot for. semantic.color.surface is not the page background; elevation.0.surface is. Bind it, or keep it as a css-variables-only token on purpose. check --json carries the same report as adoption.

5. Tighten as trust grows#

Where a derived value contradicts your system, bind it with one alias, versioned and visible. When your vocabulary is regular (a ramp per role), one bindings rule binds the whole grid instead of one alias per cell. When bindings stabilize, encode policy:

"derivation": { "require": ["semantic.color.primary", "semantic.color.danger"] }

Now a build fails if someone deletes the binding and derivation silently takes over. Your migration is done when the report’s authored/derived split matches your intent, not when it hits 100% authored. Most systems settle around 40–60% authored; the rest is coherent derivation that tracks your brand automatically. To see which slots deserve a decision first, run npx transtyle check --completeness complete: it lists them in order (completeness levels), and "require": ["completeness:recommended"] makes a whole level policy.

Coming from Tokens Studio#

If your tokens live in Tokens Studio for Figma, skip steps 1 and 2: point Transtyle at the folder the plugin syncs to (or a single-file export) and keep editing in Figma. The export is read as it is, nothing in it is rewritten.

  1. Add the export as a layer, and say which tier each set belongs to. Primitives go to option (the default), your alias sets to semantic:

    "tokens": [
      {
        "tokensStudio": "tokens/figma",
        "themes": { "Mode": { "dimension": "color-scheme", "map": { "Light": "light", "Dark": "dark" } } },
        "sets": { "core": "option", "semantic/*": "semantic" }
      },
      "tokens/transtyle.bindings.tokens.json"
    ]
  2. Map each theme group to a mode dimension declared under modes. A group you only ship one theme of (a brand) is { "fixed": "Acme" }. Your sets and their order decide the values, exactly as in Tokens Studio.

  3. Write the bindings file (step 3 above) against the placed paths: a token Figma calls color.action.default in a semantic set is {semantic.color.action.default}.

  4. Build. npx transtyle explain semantic.color.primary.solid follows the binding to the set file and the Figma path it came from, in each mode (--mode dark).

Math, references inside values, Figma weight names, unitless pixels and the legacy value / type format need nothing from you. A color modifier (hover states made with “darken” in the plugin) stops the build with TST1007: let derivation make the state colors, or author them. Each code is on the diagnostics page.

The two mistakes to avoid#

Practitioner warning

The failures we see are never technical. They’re these two, both preventable on day one.

Don’t rename your system into our catalog. The catalog names never leak into your design language. They’re the compilation interface. If your team says “flame”, your tokens say “flame” forever. Don’t bind by name similarity. Your “secondary” and shadcn’s --secondary and Bootstrap’s $secondary are three different concepts that happen to share a word, so bind by meaning, and read the language reference before assuming. check notes the two known cases (TST1124): secondary.solid or accent.solid bound to a token of that name whose value is the gray or the wash shadcn means by the word. The Cathode example runs this whole playbook against a maximally alien system, with file layout included.