A rebrand looked trivial: the client wanted their accent color changed, so I opened the token file, edited one line, and hit refresh. Buttons updated, links updated, badges updated. But a few places stubbornly kept the old color — an icon in the header, the underline beneath a heading, one tiny chart. No console error, no warning, nothing red. Just the wrong color, alive where it should have been dead.
The whole trigger was this single line:
:root {
/* was #C3F73A, now the new brand color */
--brand: #C3F73A;
}I assumed swapping that one value was enough. It wasn't. Only the elements that actually read var(--brand) changed. Everything that stored the color as a literal just sat there in its own shade.
Why this happens
A CSS custom property is not a global find-and-replace. It's just a variable. Only code that explicitly writes var(--brand) follows along when I change its value. Anything that writes the color as a literal — whether #C3F73A or rgb(195, 247, 58) — has no idea a token called --brand even exists. They aren't connected. Changing --brand doesn't touch them at all.
And the old literal turned out to hide in more places than I expected:
- Compiled CSS. Several lines in the built output carried the hex directly instead of
var(--brand)— either because a component hardcoded it, or because a build step inlined the value. - JS bundles. There were inline color strings buried in the bundle — used to set styles via JavaScript, paint a canvas, or fill in chart config. To the bundler those are just plain strings, not references to a token.
- SVG attributes.
fill="..."andstroke="..."on icons and illustrations store the color right in the markup. That was the source of my stubborn header icon. - Inline-style fallbacks. A few elements had an inline
style="..."fallback with the old color baked in.
But the real reason this slipped past me wasn't just the number of places. My audit method was wrong. I grepped the project for the hex string only:
# what I did — and it wasn't enough
grep -rin "#C3F73A" src/That search missed two things at once. First, plenty of those colors weren't written as hex — they were written as a decimal RGB triplet, 195, 247, 58, which will never match the #C3F73A pattern. Second, I limited the search to src/ and instinctively skipped *compiled* and *.min files because "those are just build output." That's exactly where the literals were hiding.
One more thing burned some of my time: not every "wrong color" was actually a wrong color. One element had a hex that matched the others exactly, yet still looked different — brighter, more electric. That wasn't a code bug. It's the Helmholtz-Kohlrausch effect: the same hex can look different depending on context — as thin text on a dark background it reads as more vivid than the same hex as a large solid block. The eye lying, not the hex being wrong. No code change can "fix" that, and I nearly went tweaking a value that was already correct.
The fix
First, fix the grep. Search for both the hex AND the decimal RGB triplet, and exclude nothing:
# search both color forms, across ALL files including build output
grep -rin -e "#C3F73A" -e "195, *247, *58" .Then read the compiled CSS line by line — don't filter it out. The *.min and *compiled* files are exactly what you must read, not skip. After that, sweep SVG attributes and JS string literals separately, because neither shows up if you only trust the instinct that "color lives in CSS."
In the end the old color turned up in three kinds of places, each fixed a different way:
<!-- SVG: replace the literal attribute with currentColor -->
<svg class="brand-icon">
<!-- before: fill="#C3F73A" -->
<path fill="currentColor" d="..." />
</svg>/* compiled CSS / inline-style: point at the token, not a literal */
.brand-icon { color: var(--brand); }
.underline { border-color: var(--brand); }// JS: don't bake in hex, read the token at runtime
const brand = getComputedStyle(document.documentElement)
.getPropertyValue("--brand")
.trim();
chart.setColor(brand); // not chart.setColor("#C3F73A")For SVG, fill="currentColor" lets the icon inherit color from CSS, and that color itself points at var(--brand). For JS, instead of baking in a color string I read the token value from :root at runtime, so the single source of truth stays --brand. Once all three were rerouted to the token, changing the brand color really was a one-line edit.
The takeaway
Swapping a --token only reaches var() consumers. A token isn't a magic broom that repaints the whole project — it only touches code that already asks it for a value. To truly rebrand a color, you have to hunt every hardcoded literal across CSS, JS, and SVG — and you have to search by hex AND by RGB triplet, because the same color can be written two ways. Don't exclude build output from the search; that's where the literals hide most often. And finally, separate a true color difference (a different hex, alpha, or gradient stop) from a perceptual one (the same hex that looks different because of context). The first is fixable in code. The second never will be, no matter how hard you tweak the value.
