Tamirlan Askar · Product Design Lead
From 20 years of hardcoded hex to one-switch theming
Summary
- Context
- Yandex Search and Alice share one design system. I own its color foundation, working with one front-end engineer.
- Problem
- Search carried 20 years of copied hex codes. Nobody could tell a deliberate color change from an accident, and on Search a wrong color can move revenue.
- What I did
- Split the colors into a named palette and roles (like "Text primary") that point to it; designers see only the roles
- Synced Figma and code 1:1 with my front-end partner
- Built an LLM diff dashboard to review the migration, and held risky changes behind a test
- Result
- 106 pseudo-semantic tokens became 53 roles, 100% design↔code sync on shipped tokens, and one switch re-skins a whole product (it used to be per-component).
Situation
My first Yandex project was one design system for Alice and Search, where the two products differ only in color tokens. Search had 20 years of copied hex. "Background primary" was a raw hex, desktop and touch kept separate copies of one token, and the front end re-hardcoded Figma colors. I called the old system unworkable and took it on.
Goal: One color layer for Alice and Search, synced 1:1 with code so a product re-skins in one switch, and a Search migration where nothing that could move a metric ships untested.
Action
Two tiers: a named palette, and roles that point to it
Why. When every role holds its own raw hex, one black lives in many places and nobody knows which is meant. Primitives are the named palette (Black/100). Roles say what a color is for (Text primary) and point to a primitive, so every color has one home. Designers see only the roles, so a raw value is far less likely to slip in.
Result. Starting from library analytics (which components are used, which colors they pull), I cleaned the whole library: 68 raw hex and opacity values became 60 named primitives, 106 pseudo-semantic tokens became 53 roles, and the 10 desktop-only copies are gone.

Sync Figma and code 1:1, one token layer for both
Why. The front end re-hardcoded Figma colors, so design and code drifted, and re-theming meant component-by-component edits.
Result. Figma is the source of truth: I own the Figma side, my front-end partner the code side, and changes flow both ways, so every shipped token has the same name in both (100%). Aligning how tokens nest with a neighboring team also gave the front end an intermediate token layer, so one switch between two brand options re-skins the whole product.
Make the migration reviewable, and ship risky colors only through a test
Why. Mid-migration, designers couldn't tell by eye what had moved where between two Figma collections. And on Search a wrong color can move revenue, so a risky change can't ship in one big re-color.
Result. In under 20 minutes I built a dashboard: Codex 5.5 reads both libraries' JSON, maps every old token to its new one, and flags what moved, disappeared or looks inconsistent; the team checks each flag by hand. It caught a gap a line-by-line diff misses (White/75, but no Black/75) and tested our migration doc against the data: the doc claimed 61 primitives where the export had 60, and merges like #111112 → #000000 stretched our own "under 5 RGB apart" rule.

Every class of change then got its own risk handler. Stage 1 shipped directly what can't move a metric: renames, the tier split, dead tokens, the desktop/touch merge; legacy colors of unknown origin were traced in code and deleted if dead. Stage 2 holds anything that could change behavior, and so CTR or revenue, behind a test that is still running.
Result
- 68 → 60 raw hex + opacity values became named primitives
- 106 → 53 pseudo-semantic tokens became roles
- 100% design↔code sync on shipped tokens; metric-sensitive colors finish via tests
- 1 switch re-skins a whole product, instead of per-component edits
What I'd do differently
- Fix the taxonomy first. Define "same semantic role" before mapping any color. Our merge rules partly emerged mid-migration.
- Track coverage from day one, per product and component, so progress needs no manual counting.