We had a problem. Across 148 .tsx files, there were 3,796 hardcoded color values — hex codes like #0f131c, #8a8f98, and #d2f800 scattered everywhere. Changing the brand accent color would mean manually updating hundreds of files. Dark mode? Forget about it.
This is the story of how we replaced every single one with CSS custom properties — and why the before/after difference is dramatic.
THE PROBLEM: HARDCODED EVERYWHERE
Here's what our code looked like before:
// ❌ Before: Hardcoded hex values
<div className="bg-[#0f131c] border-[#d2f800] text-[#8a8f98]">
<h2 className="text-[#ffffff]">Dashboard</h2>
<p className="text-[#8a8f98]">Welcome back</p>
<button className="bg-[#d2f800] text-[#000000]">Action</button>
</div>
Problems with this approach:
- No single source of truth — Changing a color means find-and-replace across 148 files
- No semantic meaning — Is
#0f131cthe card background or something else? - No theme support — Dark/light mode would require duplicating every color
- Inconsistency — Different devs used slightly different hex values for the same purpose
THE TOKEN MAPPING
We defined a mapping from hex values to semantic CSS variables in globals.css:
:root {
--background: 8 12 20; /* #080c14 - Page background */
--card: 15 19 28; /* #0f131c - Card surfaces */
--secondary: 18 22 32; /* #121620 - Secondary surfaces */
--foreground: 255 255 255; /* #ffffff - Primary text */
--muted-foreground: 138 143 152; /* #8a8f98 - Secondary text */
--primary: 210 248 0; /* #d2f800 - Brand accent */
--border: 255 255 255; /* with /10 opacity */
}
Then created Tailwind utility classes that reference these variables:
| Hardcoded | Token | Purpose |
|---|---|---|
bg-[#080c14] | bg-background | Page background |
bg-[#0f131c] | bg-card | Card surfaces |
bg-[#121620] | bg-secondary | Secondary surfaces |
text-[#ffffff] | text-foreground | Primary text |
text-[#8a8f98] | text-muted-foreground | Secondary text |
bg-[#d2f800] | bg-primary | Brand accent |
border-[#d2f800] | border-primary | Accent border |
border-white/10 | border-border | Default border |
THE MIGRATION PROCESS
We didn't do this manually. We wrote a codemod script that:
- Scanned all .tsx files for Tailwind arbitrary value classes containing hex colors
- Mapped each hex value to its semantic equivalent using the token table
- Replaced in-place while preserving all other classes and modifiers (hover:, focus:, md:)
- Generated a report showing exactly what changed in each file
BEFORE VS. AFTER
// ❌ Before
<div className="bg-[#0f131c] border border-white/10 p-8
hover:border-[#d2f800] transition-colors">
<h3 className="text-xl font-bold text-[#ffffff]">Title</h3>
<p className="text-[#8a8f98] text-sm">Description</p>
</div>
// ✅ After
<div className="bg-card border border-border p-8
hover:border-primary transition-colors">
<h3 className="text-xl font-bold text-foreground">Title</h3>
<p className="text-muted-foreground text-sm">Description</p>
</div>
The "after" version is more readable, more maintainable, and enables theming.
THE NUMBERS
- 148 files modified
- 3,796 hardcoded color instances replaced
- 8 semantic tokens cover 95% of all color usage
- 0 visual changes — the app looks identical (that's the point)
WHAT THIS ENABLES
With design tokens in place, we can now:
- Change the brand color by editing one CSS variable
- Add light mode by defining a
[data-theme="light"]set of overrides - White-label the platform for enterprise clients with custom color schemes
- Maintain consistency — new components use tokens by default
This refactor produced zero user-visible changes and took significant effort. But it's one of those invisible improvements that makes everything else easier going forward. If your codebase has more than 50 hardcoded color values, it's time to tokenize.



