Tags design tokensdesign systemsrefactoringcodemodreact-native
All posts

design tokens

The design tokens existed. The code just did not use them

An app with design tokens still had 953 hardcoded colors, and 349 of them were values the tokens already defined. Automating the swap went wrong twice.

August 18, 2026 5 min

I set out to find UI worth extracting into shared components. What I found instead was that the design tokens were already there and the code was ignoring them.

A third of the hardcoded colors were already design tokens

I counted every string literal sitting in a color, backgroundColor, or borderColor property across the app source, excluding the design tokens file itself and the 3D material code.

hardcoded colors     953
stylesheets          165

Then I did the part that mattered. I built a table of every value the token file defines, normalized both sides so that #fff and rgba(255,255,255,1) compare equal, and checked how many of the 953 were values the tokens already had.

values a token already defined    349  (36%)

One in three. There were 349 places where someone typed out the value of whiteAlpha55 by hand while whiteAlpha55 sat right there in the token file.

This is not a gap in the token set. It happens because typing the literal is faster than importing the token, and nothing in the toolchain notices the difference.

Twenty-three styles with the same name held twenty different values

The worse finding was not numeric. A style named sectionLabel existed in 23 files, and they disagreed.

files defining it23
distinct values20
most-shared value2 files

Some used #888, others rgba(255,255,255,0.7), others .55. Font size was 11 in some places and 9 in others. Some uppercased the text and some did not.

Same concept, different result on every screen. And no checker will ever flag it, because all twenty variants are perfectly valid code.

Equal values are not equal roles

I wrote a codemod for the 349 sites: replace a literal with a token reference only when the values are byte-for-byte equivalent. That felt about as safe as a refactor gets.

Then I read the output and found this:

- backgroundColor: '#fff',
+ backgroundColor: colors.brand.onPrimary,

The value is right. brand.onPrimary is white.

But that token means “the color of text and icons sitting on top of a purple surface.” It is a foreground role, and the codemod had just planted it in a background slot.

Nothing breaks today. It breaks the next time someone tunes button label contrast, because adjusting onPrimary will silently repaint a set of unrelated white backgrounds.

I reverted and added a rule: tokens whose names denote a foreground role cannot be substituted into background properties, and text tokens are excluded from backgrounds entirely. That dropped the safely-automatable set from 349 to 182.

Losing 167 sites looks like a worse outcome. Those 167 are exactly the ones that needed a human to decide the role.

Proving the pixels did not move

A machine had rewritten 51 files, so I needed evidence that nothing changed visually. Reading 51 diffs by eye is not evidence.

Instead I extracted, for each file, the ordered sequence of color slots, resolved every token reference back to its literal value, and compared the before and after sequences.

color slots compared    807  across 51 files
files whose sequence changed    0

The useful property here is that it does not depend on reading the diff. A diff tells you what changed; the question was whether the result was identical.

Consolidating the twenty-three lost spacing on ten screens

I collapsed the 23 sectionLabel definitions into one component so that color, size, and letter spacing come from a single place.

The next day a screenshot came back from a real device with the section heading jammed against the element above it.

The migration script carried over color and size and never looked at marginTop. Ten of those screens had one, with values ranging from 6 to 28, and because they all differed I never thought to pick a default.

screens that had marginTop     10
default in the new component   none

The part worth keeping is why nothing caught it.

Type checking passes, because a missing style property is not a type error. Lint passes, because the syntax is fine. The screen renders, just tighter than before.

There is no machine-readable signal at all for this class of defect. It surfaced when a person looked at a phone.

A mechanical refactor moves exactly what you told it to move. What it drops was never missing from the code, it was missing from the list, and I wrote the list.

A gate that demands zero gets ignored

766 hardcoded colors remain. I wanted a check that requires zero, and a check that requires zero fails on the day you add it.

This repository already learned that lesson once. I wrote about a related failure in turning off the auto popup deleted the feature, where there was no check at all and an entire feature quietly disappeared. A rule banning a specific character in prose, if pointed at every file at once, flagged 16,000 lines and became impossible to pass. A gate nobody can pass is a gate everybody skips.

So the current number became a ceiling instead.

pnpm design:check   # exits 1 if the count rises above 766

Adding a hardcoded color fails the build. Removing some prints a reminder to lower the baseline. New screens are held to the rule immediately; the 766 older sites come down when there is time.

I also verified the gate actually fires by putting a literal back. The count rose by 8 and it exited 1. An unverified gate is a comment.

What this is worth

The numbers come from one React Native app with 165 stylesheets, measured on 2026-08-18. Whether 36% is typical of other codebases, I have no idea.

I also do not know yet whether ratcheting a baseline actually drives 766 downward. I built it today, so there is not a single day of evidence. If the number simply freezes there, the approach was wrong and I will write that up instead.

Checking whether your own design system is being used is cheap. Count the hardcoded values, then count how many of them the tokens already define. I had no idea this repository had a problem until I measured that ratio.

Previous uniq Says Two Different Korean Lines Are Duplicates