A hard-coded colour is a value written straight into code, #2563eb instead of a token, and it quietly opts that element out of the design system. It won't follow a rebrand or a dark theme. This note gives you search commands that find them in a React codebase in minutes, the cases that produce false alarms, and how to stop new ones at review time.
Key takeaways
- Search for five kinds of colour: hex, colour functions, named colours, raw palette tokens and framework escape hatches such as Tailwind'sbg-[#…].
- Exclude generated code and comments before you count, or the numbers will lie.
- Turn the search into a lint rule so the count only goes down.
- A hard-coded colour isn't always wrong; allow exceptions with a written reason.
What counts as a hard-coded colour
CSS accepts more colour syntaxes than people remember. CSS Color 4 lists hex with 3, 4, 6 or 8 digits, rgb() and rgba(), hsl() and hsla(), hwb(), lab(), lch(), oklab(), oklch() and color() (CSS Color Module Level 4). On top of that come about 150 named colours (MDN: named-color).
In a React codebase, they hide in five places:
The last two are easy to miss. A primitive palette token like gray.100 looks like a token but behaves like a hard-coded value: it keeps the same value in every theme. Tailwind's square brackets "generate a class on the fly with any arbitrary value" (Tailwind: arbitrary values), which is handy but bypasses the theme entirely.
Search commands for each kind of colour
ripgrep searches a whole repository quickly and respects .gitignore, so build output stays out by default. Its default regex engine has no lookbehind, which is why the hex pattern uses a character class instead. Start with the two patterns that catch most cases:
# Hex colours, not preceded by a letter, & or ( (skips – and issue refs like (#619))
rg -n -e '(^|[^&\w(])#[0-9a-fA-F]{3,8}\b' src
# Colour functions (color( is left out: it matches too many JavaScript calls)
rg -n -e '\b(rgba?|hsla?|hwb|lab|lch|oklab|oklch)\(' srcThen the React-specific ones:
# Raw palette tokens in style props (adjust the palette names to your theme)
rg -n -e '["'"'"'`](gray|red|blue|green)\.\d{2,3}["'"'"'`]' src
# Tailwind arbitrary colours
rg -n -o -e '\b(bg|text|border|fill|stroke)-\[#[0-9a-fA-F]{3,8}\]' src
# Named colours in JS style props (a bare word search drowns in noise; plain CSS needs a Stylelint rule)
rg -n -e '\b(color|background|backgroundColor|bg|borderColor|fill|stroke)\s*[:=]\s*\{?\s*["'"'"'](white|black|red|blue|green|gray|grey)["'"'"']' srcOn a small test project with one of each case, these five commands found every hard-coded value, plus three false alarms that the next section removes.
Remove the noise
A raw count is almost always too high. Three sources inflate it:
- Comments. A comment such as
// Fixes the bug from #619matches the hex pattern. We hit exactly this in our own CSS check, where a(#619)inside a comment was reported as a colour. Drop comment lines before counting. - Generated code. Icon components exported from Figma carry their original colours. When we first measured a demo repository, the icons our own sync had exported counted as about 590 hard-coded colours. The code was fine; the metric wasn't. Exclude generated folders with a glob.
- The theme itself. The file that maps semantic tokens to the palette must mention the palette. Exclude it by name.
Put together, with a count per file:
rg -n --no-heading -g '!src/icons/**' -g '!src/theme.ts' \
-e '(^|[^&\w(])#[0-9a-fA-F]{3,8}\b' \
-e '\b(rgba?|hsla?|hwb|lab|lch|oklab|oklch)\(' src \
| rg -v '^[^:]+:\d+:\s*(//|/\*|\*)' \
| cut -d: -f1 | sort | uniq -c | sort -nrThe output is a ranked list of files; start at the top. The filter drops whole-line comments only, so trailing comments still count: skim the first results before trusting the total.
Decide what's allowed
Not every hard-coded colour is a mistake. A brand logo's exact colour, the white canvas of an email template, the black behind a video player: these are values, not roles. The problem is an exception nobody can see.
Our rule is simple: an exception gets a comment with a reason on the same line or the line above, and the check skips it.
// design-tokens-allow: many email clients don't support CSS variables
const EMAIL_CANVAS = "#ffffff";A reviewer can read every exception with one search, and "why is this hard-coded?" has an answer in the code.
Stop new ones at review time
A search tells you where you are. A lint rule keeps you from sliding back.
- Stylelint. The built-in
color-no-hexrule does what its name says. The pluginstylelint-declaration-strict-valuegoes further: it requires a variable, function or keyword for the properties you list, such ascolor,fillandstroke. - ESLint. Atlassian's
ensure-design-token-usagerule flags hard-coded colours and raw spacing in JavaScript and enforces itstoken()function. It's specific to Atlassian's tokens, but it shows the shape of a rule worth writing for your own. - A script in CI. If your styling lives in props, a short script built on the patterns above is often the fastest route. Ours runs as
pnpm design:checkin CI on every pull request and before every commit.
Start the rule in warning mode, fix the top files from your ranked list, then switch it to error. The count should only go down from there.
Where hard-coded colours fit in a health check
Hard-coded colours are one kind of bypass: code that skips the system even when a token exists. Counting them is one of the four signals in our guide to measuring design system health, and it's the part of an audit you can run tonight. The design system audit in one afternoon puts it next to the other checks.



