Find and fix hardcoded colors, Tailwind color classes, and repeated class patterns in web codebases.
hardcode-replacer scans your project for hardcoded color values, compares them against your design token / CSS variable palette, and gives you exact replacement code. It understands context — it knows the difference between a color that needs replacing and one that's a theme definition, a canvas fallback, or an effect overlay.
Built for humans and AI coding assistants (Claude, Copilot, Cursor) to use together. Designed to save context window space with pre-classified, actionable output.
Large web projects accumulate hardcoded colors everywhere — inline styles, CSS files, Tailwind arbitrary values, theme files. Replacing them manually means:
- Finding every
#10b981,rgba(16, 185, 129, 0.4), andbg-[#10b981]across hundreds of files - Knowing which ones are actually replaceable (not canvas code, not theme definitions, not black/white effects)
- Matching each color to the right CSS variable from your design system
- Writing the correct replacement syntax (
var(),color-mix(), etc.)
hardcode-replacer does all four in a single command.
# Install globally from npm
npm install -g hardcode-replacer
# Or use npx without installing
npx hardcode-replacer colors src/
# Find all hardcoded colors
hardcode-replacer colors src/
# Compare against your design tokens
hardcode-replacer compare src/ --vars styles/variables.css
# Auto-fix exact matches
hardcode-replacer compare src/ --vars styles/variables.css --fix
# Find Tailwind color classes
hardcode-replacer tailwind src/
# Find repeated class patterns
hardcode-replacer patterns src/
# Find repeated JSX structures for component extraction
hardcode-replacer jsx src/Scans for hex, rgb, rgba, hsl, hsla, oklch, oklab, lch, lab, hwb, color(), and CSS named colors. Supports both legacy (rgba(255, 0, 0, 0.5)) and modern (rgb(255 0 0 / 50%)) syntax.
hardcode-replacer colors [paths...] [options]Options:
| Flag | Description |
|---|---|
--include <glob> |
File pattern to include (e.g., "*.tsx") |
--exclude <glob> |
File pattern to exclude (repeatable) |
--format json |
Output as structured JSON |
--no-named |
Skip CSS named color detection |
Example output:
=== Hardcoded Colors ===
Found 669 hardcoded color values in 16 files
Actionable: 275 | Skipped: 394
Types: hex(493), rgba(162), rgb(11), named(1), oklch(2)
FILE: src/styles/theme-variables.css
L504:17 rgba(128, 255, 192, 0.15) (rgba -> #80ffc0)
0px 0px 1px rgba(128, 255, 192, 0.15),
--- Skipped 394 non-actionable colors ---
[CANVAS/WEBGL] 50 colors in 4 files
[THEME DEFINITION] 75 colors in 4 files
[CSS VAR DEFINITION] 207 colors in 3 files
[EFFECT (black/white alpha)] 21 colors in 2 files
The core command. Compares every discovered color against a variables file and produces exact replacement code.
hardcode-replacer compare [paths...] --vars <file> [options]Options:
| Flag | Description |
|---|---|
--vars <file> |
(Required) Path to CSS, JSON, JS, or TS variables file |
--threshold <n> |
Delta-E distance for "close" match (default: 10) |
--fix |
Auto-replace exact matches with var() / color-mix() |
--baseline <file> |
Save results to a baseline JSON file |
--diff <file> |
Compare against baseline, show only new issues |
--include <glob> |
File pattern to include |
--exclude <glob> |
File pattern to exclude (repeatable) |
--format json |
Output as structured JSON |
Features:
- Semantic matching — When multiple variables match the same hex value, prefers variables whose name matches the CSS property context (e.g.,
border-color: #10b981prefers--border-accent-strongover--bg-success) - color-mix() suggestions — For rgba/hsla values with alpha, suggests
color-mix(in srgb, var(--name) X%, transparent) - Variable name suggestions — For unmatched colors, suggests descriptive names based on hue and context (e.g.,
[suggest: --color-red-700]) - Auto-fix —
--fixwrites the replacements directly to files - Baseline/diff — Track progress across refactoring sessions
Example output:
=== Color Variable Comparison ===
Palette: 124 variables from theme-variables.css
Actionable: 42 exact | 83 close | 150 unmatched
--- ACTIONABLE EXACT MATCHES (42) ---
src/utils/colors.ts:34:11 #6b7280 -> --gray-500 (#6b7280) | replace: var(--gray-500)
src/utils/colors.ts:37:11 #1f2937 -> --bg-nav (#1f2937) | replace: var(--bg-nav)
--- ACTIONABLE CLOSE MATCHES (83) ---
src/utils/colors.ts:22:11 #475569 -> use --variant-muted-border (#4b5563, dE=3.9) | replace: var(--variant-muted-border)
--- ACTIONABLE UNMATCHED (150) ---
src/tailwind.css:170:54 rgb(199 14 14 / 49%) -> #c70e0e (nearest: --status-negative dE=17.78) [suggest: --color-red-500]
Supported variable file formats:
| Format | Example |
|---|---|
| CSS custom properties | --primary-500: #10b981; |
| JSON (flat or nested) | { "primary": { "500": "#10b981" } } |
| JS/TS exports | primary500: '#10b981' |
Finds Tailwind color utility classes and optionally checks arbitrary values against your theme.
hardcode-replacer tailwind [paths...] [options]Options:
| Flag | Description |
|---|---|
--vars <file> |
Compare arbitrary values (e.g., bg-[#10b981]) against a palette |
--threshold <n> |
Delta-E distance for arbitrary value matching (default: 10) |
--include <glob> |
File pattern to include |
--exclude <glob> |
File pattern to exclude (repeatable) |
--format json |
Output as structured JSON |
Detects Tailwind v4 @theme and @utility directives automatically.
Example output:
=== Tailwind Color Classes ===
Found 964 Tailwind color classes in 75 files
Arbitrary values: 791
--- ARBITRARY VALUES MATCHING THEME (3) ---
src/Button.tsx:12:5 bg-[#10b981] -> EXACT: --border-accent-strong (#10b981)
Use var(--border-accent-strong) or a Tailwind theme color instead of #10b981
Finds repeated className / class strings and cn() / clsx() / twMerge() / cva() calls that could be extracted.
hardcode-replacer patterns [paths...] [options]Options:
| Flag | Description |
|---|---|
--min-count <n> |
Minimum occurrences to report (default: 2) |
--min-classes <n> |
Minimum classes in a pattern (default: 2) |
--include <glob> |
File pattern to include |
--exclude <glob> |
File pattern to exclude (repeatable) |
--format json |
Output as structured JSON |
Example output:
=== Repeated Class Patterns ===
Found 9 repeated patterns across 36 locations
1. "font-bold leading-none ml-2 px-2 text-[10px] tracking-widest uppercase"
Classes: 9 | Occurrences: 4 | Impact score: 36
Sources: className, cn/clsx
Consider: CVA variant, @apply directive, or component extraction
--- Frequently Co-occurring Class Pairs ---
"flex items-center" (53 co-occurrences)
"font-mono text-xs" (25 co-occurrences)
Finds repeated JSX subtrees (tag hierarchies + classNames) that can be extracted into reusable components. Unlike patterns (which finds repeated className strings), jsx detects repeated structural patterns — e.g., a <button><span>...</span></button> used in 5 places with similar classNames but different text content.
hardcode-replacer jsx [paths...] [options]Options:
| Flag | Description |
|---|---|
--min-count <n> |
Minimum occurrences to report (default: 2) |
--min-depth <n> |
Minimum subtree depth (1 = parent+child) (default: 1) |
--similarity <n> |
Jaccard similarity threshold for "similar" clusters, 0-1 (default: 0.7) |
--include <glob> |
File pattern to include |
--exclude <glob> |
File pattern to exclude (repeatable) |
--format json |
Output as structured JSON |
Three cluster types:
- Exact — Identical tag hierarchy AND classNames
- Structural — Same tag hierarchy, different classNames (extract with variant props)
- Similar — Same root tag, 70%+ className overlap (Jaccard similarity)
Example output:
=== Repeated JSX Structures ===
Scanned 69 files. Found 89 clusters.
Exact duplicates: 72 (identical structure + classes)
Structural duplicates: 15 (same tags, different classes)
Similar patterns: 2 (same root, 70%+ class overlap)
1. [EXACT] 5x across 3 file(s) — impact: 30
Fingerprint: button(span)
<button .bg-grey-800 .border .border-grey-700 ...>
<span .font-retron .text-sm .text-white ...></span>
</button>
Shared classes: bg-grey-800, border, border-grey-700, flex, ...
→ Extract into a reusable component. 5 identical instances found.
The tool classifies every found color to separate actionable results from noise. This is what makes it practical for large codebases.
| Context | What it means | Example |
|---|---|---|
| ACTIONABLE | Can be replaced with var() |
color: #10b981; in a component |
| CSS VAR DEFINITION | This IS a variable definition | --primary: #10b981; |
| THEME DEFINITION | In a theme/token file | colors.ts, theme.tsx, palette.js |
| CANVAS/WEBGL | In a canvas context (no CSS vars) | Files importing three, d3, sharp |
| MAPPING/LOOKUP | Used as a lookup key | '#10b981': 'success' |
| GENERATED | Template-generated code | `<div style="${color}">` |
| META/MANIFEST | Browser-level (no CSS) | <meta name="theme-color" content="#10b981"> |
| EFFECT | Intentional black/white + alpha | rgba(0,0,0,0.4) in shadows/overlays |
Detection is automatic based on:
- File-level analysis: Imports (three.js, d3, sharp), file path patterns (theme/, colors.ts), content patterns (createTheme, getCssVar)
- Line-level heuristics: CSS variable definitions, object key positions, template literals, meta tags
- Multi-line comment detection: Tracks
/* ... */block comment ranges
Create .hardcode-replacerrc.json in your project root for default settings:
{
"exclude": ["**/*.test.*", "**/*.stories.*"],
"include": "*.{tsx,jsx}",
"vars": "src/styles/variables.css",
"threshold": 10,
"named": true,
"minCount": 2,
"minClasses": 3,
"tailwindVersion": 4
}Also supports .hardcode-replacerrc (JSON) and hardcode-replacer.config.js (CommonJS).
CLI options always override config file values.
hardcode-replacer is designed to be called by Claude, Copilot, or Cursor via bash. A typical refactoring session:
# Step 1: Scan the codebase
hardcode-replacer compare src/ --vars styles/variables.css --format json
# Step 2: Auto-fix the easy wins (exact matches)
hardcode-replacer compare src/ --vars styles/variables.css --fix
# Step 3: Save a baseline for tracking progress
hardcode-replacer compare src/ --vars styles/variables.css --baseline .hardcode-baseline.json
# Step 4: After manual fixes, check what's left
hardcode-replacer compare src/ --vars styles/variables.css --diff .hardcode-baseline.json
# Step 5: Find Tailwind classes using hardcoded colors
hardcode-replacer tailwind src/ --vars styles/variables.css
# Step 6: Find class patterns to extract into components
hardcode-replacer patterns src/ --min-count 3 --min-classes 3
# Step 7: Find repeated JSX structures for component extraction
hardcode-replacer jsx src/ --min-depth 2The --format json flag produces structured output suitable for programmatic consumption by AI tools. The text format is optimized for humans and for pasting into chat.
# Quick scan: what's the damage?
hardcode-replacer colors src/
# How many match our theme?
hardcode-replacer compare src/ --vars styles/theme.css
# Fix all exact matches automatically
hardcode-replacer compare src/ --vars styles/theme.css --fix
# Review the close matches (might need design decisions)
hardcode-replacer compare src/ --vars styles/theme.css --threshold 5
# Find repeated Tailwind patterns for extraction
hardcode-replacer patterns src/ --min-count 3
# Find JSX structures to extract into components
hardcode-replacer jsx src/ --min-count 3Colors are matched using CIE76 Delta-E perceptual distance:
| Delta-E | Perception |
|---|---|
| 0 | Identical |
| < 1 | Imperceptible difference |
| 1-2 | Close — probably the same intended color |
| 2-10 | Noticeable — likely a deviation from the palette |
| > 10 | Different color |
The default threshold of 10 catches colors that are "in the neighborhood" of a palette color but clearly deviant. Lower it to 5 for stricter matching.
| Format | Example | Parsed? |
|---|---|---|
| Hex (3/4/6/8 digit) | #fff, #10b981, #10b98180 |
Yes |
| RGB (legacy) | rgb(16, 185, 129) |
Yes |
| RGBA (legacy) | rgba(16, 185, 129, 0.5) |
Yes |
| RGB (modern) | rgb(16 185 129) |
Yes |
| RGB (modern + alpha) | rgb(16 185 129 / 50%) |
Yes |
| HSL (legacy) | hsl(160, 84%, 39%) |
Yes |
| HSLA (legacy) | hsla(160, 84%, 39%, 0.5) |
Yes |
| HSL (modern) | hsl(160 84% 39%) |
Yes |
| HSL (modern + alpha) | hsl(160 84% 39% / 50%) |
Yes |
| OKLCH | oklch(0.88 0.05 143) |
Detected |
| OKLAB | oklab(0.88 0.05 0.02) |
Detected |
| LCH / LAB / HWB | lch(...), lab(...), hwb(...) |
Detected |
CSS color() |
color(srgb 0.1 0.7 0.5) |
Detected |
| CSS named colors | red, cornflowerblue |
Yes (148 colors) |
"Detected" means the value is found and reported but not yet converted to hex for palette matching. Hex/RGB/HSL/named are fully parsed and matched.
src/
cli.js Commander setup, all commands and options
config.js Config file loader (.hardcode-replacerrc)
search.js Ripgrep/grep wrapper (execFileSync, no shell)
color-patterns.js Regex patterns for all color formats
color-utils.js Parsing, conversion, Delta-E, alpha, suggestions
css-named-colors.js All 148 CSS named colors
tailwind-colors.js Tailwind names, prefixes, v4 detection
context-classifier.js Context classification engine
commands/
find-colors.js `colors` command
find-tailwind.js `tailwind` command
compare-vars.js `compare` command (+ fix, baseline, diff)
find-patterns.js `patterns` command
find-jsx.js `jsx` command
Search: Uses ripgrep (rg --json) for fast structured search with grep fallback. All external commands use execFileSync (no shell) to prevent command injection.
Classification: File-level analysis is cached per-file. Import scanning checks the first 100 lines for canvas/WebGL library imports. Block comment ranges are computed once per file.
# npm
npm install -g hardcode-replacer
# npx (no install)
npx hardcode-replacer colors src/- Node.js >= 18
- ripgrep (
rg) — recommended for performance, falls back to grep - No other dependencies — only
commanderfor CLI parsing
Single-string detection for in-process callers (e.g. PreToolUse hooks blocking Edit/Write before the file is saved). The CLI scans whole trees with the broader HEX_PATTERN from src/color-patterns.js; the /detect subpath exposes a narrower, configurable check intended for write-time gates.
const { detectHexViolations, isHexExemptPath } = require("hardcode-replacer/detect");
if (isHexExemptPath(filePath)) return;
const violations = detectHexViolations(sourceContent, { maxMatches: 5 });
if (violations.length > 0) {
// violations: Array<{ line: number, content: string, match: string }>
// line is 1-indexed; content has trailing whitespace trimmed
}Defaults (write-time gate semantics):
pattern—/#[0-9a-fA-F]{6}/(6-digit only;#fffshorthand passes through). PassHEX_PATTERNfromhardcode-replacer/src/color-patternsto widen.filterKeywords—["var(--", "@theme", "primitive", "allow-hex"].allow-hexis an intentional inline-comment escape (color: #ff0000; // allow-hex: legacy logo).skipComments— skips//and/* ... */block comments.maxMatches— stop scanning after N matches; default unbounded.
isHexExemptPath skips token source files (/design-tokens/, /foundation.css, …), fixture/test/stories paths, generated registries, and tailwind.config. Pass extraFragments to widen.
Used by: cli/el-hook (verticalint/tools) write-time PreToolUse gate. The CLI hardcode-replacer compare remains the batch-sweep + fuzzy-match-to-vars + auto-fix path.
PRs welcome! If you find false positives or missed patterns, please open an issue with:
- The color value that was mis-classified
- The line of code it appeared in
- The file context (imports, file path)
MIT — use it however you want.