Skip to content
academia.sh

Lesson 19 / 19

Toolchain

The single-direction source of truth between the design tool and code, normalizing the token export, eliminating false diffs, and classifying the remaining real deviation to arrive at a sync rate.

Contents

The previous lesson’s scan stayed on the code side: it checked whether the system component was used, but not whether the values it carries match the design library’s. A team may be using the system button while the button’s fill color differs from the design file’s; coverage percentage counts this as one hundred percent.

The toolchain closes this gap. Its job is to continuously verify that the values in the design tool and the values in code are the same. This lesson defines the chain’s direction, produces the diff report, and shows the normalization needed for the report to become readable.

The Direction of Truth

If a value is kept in two places, which one is true must be decided in advance. There are three arrangements, and all three are single-direction:

  • The design tool is the source. Values are defined in the design library, exported, and generated into code. Code is never edited by hand.
  • Code is the source. Values are defined in a token file and imported into the design tool. The designer changes the value in the file, not in the tool.
  • There is a neutral source. Values are defined in a file independent of both the tool and the code — a JSON document — and both the design tool and code are fed from this file.

A fourth option that looks available, two-way sync, does not work: if both sides have write access, the same token changing on both at once produces a conflict with no rule to resolve it — which value is newer does not tell you which is more correct. This is why the first decision in building a toolchain is which direction it flows; technical choices come after.

Once the direction is chosen, the chain’s shape is the same: export → transform → target formats. The transform step converts names to the target’s convention, brings values into the target’s units, and computes derived tokens. There can be more than one target format — CSS custom properties, mobile format files, and doc pages can all be generated from the same source.

Producing the Diff Report

Whichever direction is chosen, the two sides still need to be checked for sameness. The real obstacle to this check is that the two sides write the same value in a different form: the design tool writes a color in uppercase hex, code in lowercase; the design tool writes a length in pixels, code as a root-relative unit.

The program below compares eighteen design tokens against eighteen code tokens in three stages: raw, with only names normalized, and with both names and values normalized.

// toolchain.mjs — diff report between the design tool's token export and the code
// token definition

// The design tool's exported name-value pairs.
const DESIGN = {
  "Color/Surface/Primary": "#FFFFFF",
  "Color/Surface/Secondary": "#F7F7F8",
  "Color/Border/Default": "#B7BCC2",
  "Color/Text/Primary": "rgb(31, 35, 40)",
  "Color/Text/Secondary": "#5A6169",
  "Color/Action/Main": "#275EA5",
  "Color/Action/On Main": "#FFF",
  "Color/Action/Secondary": "#3B7BC8",
  "Spacing/Step 1": "4px",
  "Spacing/Step 2": "8px",
  "Spacing/Step 3": "12px",
  "Spacing/Step 4": "16px",
  "Typography/Body/Size": "16px",
  "Typography/Body/Line Height": "1.5",
  "Typography/Heading/Size": "24px",
  "Radius/Small": "4px",
  "Radius/Medium": "8px",
  "Shadow/Elevation 1": "0 1px 2px rgba(0, 0, 0, 0.08)",
};

// The custom properties defined in code.
const CODE = {
  "--color-surface-primary": "#ffffff",
  "--color-surface-secondary": "#f7f7f8",
  "--color-border-default": "#b7bcc2",
  "--color-text-primary": "#1f2328",
  "--color-text-secondary": "#5a6169",
  "--color-action-main": "#275ea5",
  "--color-action-on-main": "#ffffff",
  "--color-action-secondary": "#2f6bb5",
  "--spacing-step-1": "0.25rem",
  "--spacing-step-2": "0.5rem",
  "--spacing-step-3": "0.75rem",
  "--spacing-step-4": "1rem",
  "--typography-body-size": "1rem",
  "--typography-body-line-height": "1.5",
  "--typography-heading-size": "1.5rem",
  "--radius-small": "0.25rem",
  "--radius-medium": "0.5rem",
  "--color-action-danger": "#a12d2d",
};

// Name normalization: the same transliteration rule established in the naming lesson.
const TRANSLIT = { "’": "'", "‘": "'", "“": '"', "”": '"', "–": "-", "—": "-", "…": "..." };
const normalize = (s) => [...s].map((ch) => TRANSLIT[ch] ?? ch).join("");
const nameNormal = (designName) => "--" + normalize(designName).toLowerCase().replace(/[/\s]+/g, "-");

// Value normalization: color -> six-digit lowercase hex, length -> pixels.
const ROOT_SIZE = 16;
function valueNormal(value) {
  const v = value.trim().replace(/\s+/g, " ");
  const shortHex = v.match(/^#([0-9a-fA-F]{3})$/);
  if (shortHex) return "#" + [...shortHex[1]].map((h) => h + h).join("").toLowerCase();
  if (/^#[0-9a-fA-F]{6}$/.test(v)) return v.toLowerCase();
  const rgb = v.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
  if (rgb) return "#" + rgb.slice(1, 4).map((n) => Number(n).toString(16).padStart(2, "0")).join("");
  const rem = v.match(/^(-?[\d.]+)rem$/);
  if (rem) return `${Number(rem[1]) * ROOT_SIZE}px`;
  const px = v.match(/^(-?[\d.]+)px$/);
  if (px) return `${Number(px[1])}px`;
  return v;
}

// Three-stage comparison: raw, name normalization only, full normalization.
function compare(nameTransform, valueTransform) {
  const left = new Map(Object.entries(DESIGN).map(([a, d]) => [nameTransform(a), valueTransform(d)]));
  const right = new Map(Object.entries(CODE).map(([a, d]) => [a, valueTransform(d)]));
  const designOnly = [...left.keys()].filter((a) => !right.has(a));
  const codeOnly = [...right.keys()].filter((a) => !left.has(a));
  const valueDiff = [...left.entries()].filter(([a, d]) => right.has(a) && right.get(a) !== d).map(([a]) => a);
  return { designOnly, codeOnly, valueDiff, common: left.size - designOnly.length };
}

const stages = [
  ["raw comparison", (a) => a, (d) => d],
  ["name normalization", nameNormal, (d) => d],
  ["name + value normalization", nameNormal, valueNormal],
];

console.log("stage                       common name   design only   code only   value diff   total diff");
let last = null;
for (const [name, nT, vT] of stages) {
  const s = compare(nT, vT);
  last = s;
  const total = s.designOnly.length + s.codeOnly.length + s.valueDiff.length;
  console.log(
    `${name.padEnd(27)} ${String(s.common).padStart(11)} ${String(s.designOnly.length).padStart(13)} ` +
    `${String(s.codeOnly.length).padStart(11)} ${String(s.valueDiff.length).padStart(12)} ${String(total).padStart(11)}`
  );
}

console.log("\n— REAL DEVIATIONS —");
for (const a of last.valueDiff) {
  const designKey = Object.keys(DESIGN).find((k) => nameNormal(k) === a);
  console.log(`value diff: ${a}\n    design: ${DESIGN[designKey]} → ${valueNormal(DESIGN[designKey])}\n    code  : ${CODE[a]} → ${valueNormal(CODE[a])}`);
}
for (const a of last.designOnly) console.log(`design only: ${a} (${DESIGN[Object.keys(DESIGN).find((k) => nameNormal(k) === a)]})`);
for (const a of last.codeOnly) console.log(`code only: ${a} (${CODE[a]})`);

// Sync rate and gate: at what threshold does a release get blocked?
const totalTokens = new Set([...Object.keys(DESIGN).map(nameNormal), ...Object.keys(CODE)]).size;
const realDiff = last.designOnly.length + last.codeOnly.length + last.valueDiff.length;
console.log(`\ntotal tokens (union): ${totalTokens}`);
console.log(`real deviation: ${realDiff}   sync rate: ${(((totalTokens - realDiff) / totalTokens) * 100).toFixed(1)}%`);
stage                       common name   design only   code only   value diff   total diff
raw comparison                        0            18          18            0          36
name normalization                   17             1           1           16          18
name + value normalization           17             1           1            1           3

— REAL DEVIATIONS —
value diff: --color-action-secondary
    design: #3B7BC8 → #3b7bc8
    code  : #2f6bb5 → #2f6bb5
design only: --shadow-elevation-1 (0 1px 2px rgba(0, 0, 0, 0.08))
code only: --color-action-danger (#a12d2d)

total tokens (union): 19
real deviation: 3   sync rate: 84.2%

Eliminating False Diffs

The three-stage table explains why the diff report goes unread in most organizations. The raw comparison finds 36 diffs, and none of them are real; the two sides are using different naming conventions. A report like this gets read once, then gets closed.

Name normalization brings the diff down to 18, using the same transliteration rule established in the naming lesson — its cost was paid there, and its return is collected here. Without that convention, this step would need its own mapping table, one that would go stale as tokens were added.

Value normalization brings the diff down to 3. The 15 eliminated diffs came from three format distinctions: hex-notation letter case, three-digit short notation, the rgb functional notation, and length units written as root-relative instead of pixels. None produce a visual deviation; both sides describe the same color and the same length.

The order itself is a design decision: normalization happens before the diff is computed, not after. Computing the diff first and then trying to eliminate it means deciding which diff is false all over again every time.

Reading the Remaining Three Deviations

Each of the three remaining rows produces a different piece of work.

Value diff--color-action-secondary differs between the two sides. This is a violation of the source of truth: one side was changed without waiting for the other. Even once the source’s direction has been decided, this row alone does not say which side is correct; what it says is that the flow broke somewhere. Given that this color is the secondary action color, and that the typography and color lessons computed the contrast ratio between it and the primary action color, the deviation’s consequence may not be visual but functional.

Design only--shadow-elevation-1 exists in the design library but not in code. This is a design decision that was never implemented, and it enters the contribution process as a piece of work. This row staying in the report keeps the decision from being forgotten.

Code only--color-action-danger exists in code but not in design. A team needed a color for a destructive action and added the token on the code side. This resolves into one of two outcomes: the token is legitimate and gets added to the design library, or it is not and gets removed. Left undecided, it stays, gets used, and its counterpart cannot be found the next time a theme is derived.

The sync rate — 84.2% — is these three rows reduced to a single number, and the same gate built for name matching in the naming lesson can be built here too: when the rate falls below a floor, the release is blocked. What matters is not the number itself but that it is measured on every release; a deviation is not a one-time event, it is a quantity that accumulates.

The Layer the Chain Cannot Cover

The line between what the toolchain can and cannot do is sharp, and it explains why the structure built across this lesson cannot be built by tooling alone.

The chain syncs values: color, spacing, typography, radius, shadow — name-value pairs that can be compared in a machine and generated automatically.

The chain cannot sync behavior. Which state a button takes on which color, where a modal sends focus, how a dropdown is navigated by keyboard cannot be exported from a design tool. This layer is synced by the naming convention built in the second lesson, the doc sections defined in the third lesson, and the review gates built in the fifth lesson. The toolchain is the guardian of the token layer; the guardian of the component layer is governance.

Summary

  • If a value is kept in two places, the direction of the source of truth must be decided in advance; two-way sync is not an option because it produces unresolvable conflicts. The chain’s shape is independent of direction: export, transform, and multiple target formats.
  • A diff report is unreadable without normalization; in the example, 33 of 36 diffs were false diffs coming only from naming-convention differences.
  • Normalization happens before the comparison; the name rule is inherited from the naming convention, the value rule reduces colors to a single notation and lengths to a single unit.
  • The remaining deviations fall into three classes: a value diff shows the flow broke somewhere, a design-only token shows an unimplemented decision, a code-only token shows an unrecorded addition.
  • The sync rate is measured on every release and tied to a floor; a deviation is not an event but a quantity that accumulates.
  • The toolchain syncs values; it cannot sync behavior. The component layer’s equality is guaranteed by the naming convention, doc sections, and review gates.

Course Wrap-Up

This topic built the layer of a design system that comes after values. What enters the catalog was tied to a scope criterion, and how ready an entry is was tied to maturity levels’ exit conditions. The shared language between design and code was turned into a derivation rule, and the mismatched names were counted. Documentation was split into seven required sections, and its coverage was computed weighted by usage. The version number stopped being a debatable topic and became the result of the interface diff; migration cost was measured by call site. The contribution flow became a state machine, governance models were compared by queue and deviation counts, adoption was measured through a repository scan, and finally, the token layer’s equality was tied to a diff report.

What all this structure has in common is that every decision was tied to a number: coverage percentage, gap impact, migration cost, queue time, deviation count, sync rate. A design system’s success is tracked by these numbers; untracked, it turns into a library no one notices has been abandoned.

The catalog is built and governed. But every entry in the catalog still leaves one question unanswered: how is this component used with a keyboard, where does it send focus, how does it announce itself to a screen reader? The third lesson defined an “accessibility note” as a doc section and counted it among the stable maturity level’s exit criteria, but it never said what goes inside that note. The next course, Accessible Component Patterns, fills this gap: it spells out, one by one, the keyboard behavior, focus management, and screen-reader information for every component in the catalog, from button to dropdown, from tabs to modal; it designs focus traps for complex interaction patterns and makes interface text and localization requirements part of the system.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close