Skip to main content

Command Palette

Search for a command to run...

Session 1: Pipeline + Color Token Architecture

Updated
8 min read

Series: What If a Design System Could Self QA?
Author: C. Maldonado
Date: February 2026


The Starting Point

Our design system was in that awkward middle stage — we had Figma variables, dev had implemented their own version of our color tokens, and the two didn't quite match. The variable names were borrowed from Material Design 3 (primary, secondary, tertiary, neutral, neutral variant) which made sense to the developer who set them up, but meant nothing to a designer picking colors in Figma. "What's the difference between content.base and content.elevated?" was a question that shouldn't require a Slack thread to answer.

I wanted three things:

  1. A single source of truth for design tokens that both design and dev could trust

  2. An automated pipeline so token changes flow from Figma to code without manual handoffs

  3. Naming that communicates intent — not Material Design jargon

This is the story of building all three in one session, including every failure along the way.


Part 1: The Pipeline

Setting Up the Repo

The foundation is a GitHub repo (kwi-design-tokens) with a simple structure:

kwi-design-tokens/
├── .github/workflows/token-pipeline.yml
├── style-dictionary.config.js
├── package.json
├── tokens/
│   └── colors.json          ← hand-written clean tokens
├── tokens.json               ← auto-generated by Tokens Studio
└── build/                     ← generated output (CSS, SCSS, JS, JSON)

The pipeline uses Style Dictionary (by Amazon) to transform token JSON files into platform-specific outputs — CSS custom properties, SCSS variables, JS modules, and a flat JSON file. A GitHub Action watches for changes and auto-builds.

Four Runs to Green

Getting the pipeline to actually work was a journey. Here's the real timeline:

Run 1 — Config not found. Style Dictionary v4 doesn't auto-detect style-dictionary.config.js. The CLI just says "unable to find config file" and dies. Fix: add --config ./style-dictionary.config.js explicitly to the build command. Lesson learned — don't assume tools find your config by convention.

Run 2 — 215 broken references. I'd added tokens.json (the Tokens Studio sync file) to the Style Dictionary source array. Bad idea. The Figma Variables were messy — references like fontFamily.Base that pointed to nothing. 215 broken references. Fix: exclude tokens.json from the build source for now, keep only the clean hand-written tokens. Added a TODO comment to re-add it once variables are cleaned up.

Run 3 — Build passed, push failed. The Style Dictionary build actually worked (green check on that step!) but the GitHub Actions bot couldn't push the generated files back to the repo. "Write access to repository not granted." Exit code 128. Fix: repo Settings → Actions → General → Workflow permissions → "Read and write permissions." A one-checkbox fix that took longer to find than to apply.

Run 4 — Green. Everything passed. The full loop worked: push triggers workflow → Style Dictionary builds → bot commits generated files back to the repo. 17 seconds total.

The takeaway: Pipeline debugging is mostly about permissions, paths, and config — not the actual transformation logic. Style Dictionary itself worked perfectly once it could find its inputs and the bot could write its outputs.

Connecting Tokens Studio

Tokens Studio is a Figma plugin that lets you manage design tokens and sync them to a Git repo. The connection itself was straightforward — authenticate with GitHub, point it at the repo and branch, done. The key decision was making Tokens Studio the single source of truth, not Figma's native Variables panel. Why?

  • Tokens Studio supports the full DTCG token format with references, descriptions, and extensions

  • It pushes directly to Git, which triggers the pipeline automatically

  • Figma's native Variables panel is great for consumption but awkward for authoring complex token structures

The workflow becomes: edit in Tokens Studio → push to GitHub → pipeline builds → dev consumes output files. No Slack messages, no manual exports, no "hey can you update the colors" tickets.


Part 2: The Naming Problem

With the pipeline working, I turned to the actual token names. Our system had two layers:

Primitives — the raw color palettes with 0-100 brightness scales:

primitive.primary      → deep blues (brand color)
primitive.secondary    → blue-grays
primitive.tertiary     → pure grays
primitive.neutral      → pure grays (identical to tertiary!)
primitive.neutral variant → warm grays/tans
primitive.error/success/warning/info → feedback colors

Semantics — intent-based tokens that reference primitives:

semantic.surface.base     → {primitive.neutral.98}   (page backgrounds)
semantic.surface.primary  → {primitive.primary.60}    (brand surfaces)
semantic.content.base     → {primitive.neutral.10}    (primary text)
semantic.content.elevated → {primitive.neutral.10}    (same as base??)
semantic.content.dim      → {primitive.neutral.10}    (also same as base???)

See the problems?

Problem 1: "Primary" means two different things

At the primitive level, primary means "the brand color family." At the semantic level, surface.primary means "a brand-colored surface" — but it reads like "the most important surface" (which is actually surface.base). The word was doing double duty and confusing everyone.

Problem 2: Content tokens mirrored surface names for no reason

content.base, content.elevated, and content.dim were all mapped to the exact same color — neutral/10 (near-black). Dev had mirrored the surface naming thinking "content ON base surface, content ON elevated surface, content ON dim surface." But in light mode, dark text on any light surface is... the same dark text. Three tokens, one color, maximum confusion.

Problem 3: Tertiary and neutral were identical

Both primitive.tertiary and primitive.neutral were pure gray ramps with identical hex values at every stop. Two names for one palette.

Problem 4: M3 vocabulary isn't designer-friendly

"Neutral variant" is Material Design 3 terminology. A designer browsing the token panel shouldn't need to know M3 to understand that these are warm-toned grays.


Part 3: Intent-Based Naming

The fix was guided by one principle: names should communicate purpose, not implementation. And they should survive white-labeling — if we spin up a POS product with a different brand color, the token names should still work.

Primitive Renames

BeforeAfterWhy
primitive.primaryprimitive.brand"Brand" communicates role. If we white-label, brand maps to whatever the new color is.
primitive.secondaryprimitive.accentSupporting color family. "Accent" is universally understood.
primitive.tertiaryremovedIdentical to neutral. One gray ramp is enough.
primitive.neutralprimitive.neutralAlready clear. No change.
primitive.neutral variantprimitive.neutral-warmCommunicates the warm undertone without being hue-specific.
primitive.error/success/warning/infono changeAlready intent-based. "Error" works whether it's red, orange, or purple.

Semantic Renames

Surface (3 renames, no structural changes): surface.primarysurface.brand, surface.secondarysurface.accent, surface.tertiarysurface.muted

Content (restructured):

  • content.basecontent.default (clearer — "the default text color")

  • content.elevateddeleted (identical to base, redundant)

  • content.dimdeleted (identical to base, redundant)

  • content.primarycontent.brand (brand-colored interactive text)

  • content.secondary and content.tertiarykept (these ARE hierarchy — 2nd and 3rd level text)

Border (1 rename): border.primaryborder.brand (focus rings, active input borders)

The content hierarchy became a clean intensity ladder: default → secondary → tertiary → disabled. Any text or icon just picks its rung. No more guessing whether "elevated" is darker or lighter than "base."

State Tokens

We also cleaned up the state tokens and connected them to primitives:

TokenBeforeAfter
state.hoverhardcoded to primary/99{primitive.brand.99}
state.pressedhardcoded #1A1A1A at 12%#1A1A1A at 12% alpha (neutral/10 equivalent)
state.selectedhardcoded #004499 at 20%#004499 at 20% alpha (brand/40 equivalent)
state.focushardcoded #004499 at 20%#004499 at 20% alpha (brand/40 equivalent)

The pressed/selected/focus tokens stayed hardcoded because Tokens Studio's color modifier (reference + alpha) requires a Pro license. Added TODO descriptions to swap these to primitive references when we upgrade.


Part 4: The POC Moment

After making all the renames in Tokens Studio, I pushed to GitHub. The pipeline fired. 15 seconds later: green check.

Run #5 — "updated color tokens based on - intent not implementation"

That commit message captured the whole philosophy in one line. The pipeline took the renamed tokens, Style Dictionary transformed them into CSS/SCSS/JS/JSON, and the bot committed the output files. Zero visual changes in the product. Zero broken builds. Just better names that everyone on the team can understand.


What I Learned

Start with the pipeline, not the tokens. Getting the automation working first — even with dummy tokens — meant I could iterate on naming with confidence. Every change gets validated by the build. If I'd spent weeks perfecting token names first, I'd still be manually exporting JSON files.

Debug in public. Four failed pipeline runs sounds bad. But each failure taught something specific: config detection, reference validation, file permissions. Documenting these failures (and their fixes) in the migration guide means the next person doesn't hit the same walls.

Naming is a design decision, not a dev decision. Material Design 3 naming conventions (primary, secondary, tertiary, neutral variant) are designed for Google's ecosystem. They're not wrong — they're just not ours. Renaming tokens to match how our team thinks about color (brand, accent, muted, warm-gray) reduces cognitive overhead for every designer and developer who touches the system.

Redundancy is the enemy of clarity. Three content tokens with the same value. Two primitive palettes with identical grays. Every duplicate is a decision someone has to make that shouldn't exist. Removing them made the system simpler and the remaining tokens more meaningful.

Intent survives rebranding. Hue doesn't. The temptation was to rename primary to blue. But "blue" breaks the moment you white-label. "Brand" works forever because it describes the role, not the color. This distinction matters for any design system that might serve more than one product.

Artifacts

  • KWI-Token-Migration-Guide.docx — full migration doc with rename tables, CSS variable mapping, risk assessment, and timeline for dev coordination

  • Light.tokens.updated.json — programmatically generated token file with all renames applied (used as reference; actual renames done manually in Tokens Studio)

  • GitHub Actions pipeline — 5 runs total (4 debugging, 1 POC success)


Pipeline proven. Color tokens renamed and pushed. Migration guide ready for dev handoff.

Built with: Figma, Tokens Studio, GitHub Actions, Style Dictionary v4, Claude Code, and an unreasonable amount of patience for YAML indentation.

What If a Design System Could Self QA?

Part 2 of 4

A running diary documenting the build-out of an automated design token pipeline — from Figma to production code — with QA scripts, Claude Code as a collaborator, and a vision for AI-assisted interface assembly. Written by a designer, not an engineer.

Up next

Session 2: Automated QA — WCAG, Contracts, and Impact Tracing

Series: What If a Design System Could Self QA?Author: C. MaldonadoDate: February 2026 Where We Picked Up Between sessions, I'd done some homework. Spacing and typography tokens got created in Tokens