Matching Hex Color Codes with Regular Expressions
Hex color codes are one of the most common formats developers validate — from user-submitted theme customisations to CSS-linter rules to Figma-plugin parsers. The web platform now supports four hex forms (#RGB, #RGBA, #RRGGBB, #RRGGBBAA), so a robust regex needs to account for all of them if your input isn't strictly controlled. Below are the patterns developers reach for every day, with clear rules about what they accept and reject.
The Core Patterns
Classic 6-digit hex (strict)
/^#[A-Fa-f0-9]{6}$/ // exactly 6 hex digits after #
/^#[0-9A-F]{6}$/i // same, using the i flag
/^#[a-fA-F0-9]{6}$/ // same, character class order swappedThis is the safest choice when you know all inputs are full 6-digit codes. It rejects #FFF, #FF5733AA, and FF5733 (missing hash).
3 or 6 digit hex
/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/
// order matters: put the longer alternative FIRST so the engine
// doesn't match #FFFAAA as #FFF and stop thereCSS Level 3 introduced 3-digit shorthand hex, where #F0C expands to #FF00CC. Any modern hex regex should support both forms.
All four modern hex forms (3, 4, 6, 8 digits)
/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/
// 3-digit: #F0C → #FF00CC
// 4-digit: #F0C8 → #FF00CC88 (with alpha)
// 6-digit: #FF00CC
// 8-digit: #FF00CC88 (RRGGBBAA)The 4-digit and 8-digit forms are CSS Color Module Level 4. Chrome, Firefox and Safari have shipped them since 2019 — but if you support very old browsers, validate only 3 and 6 digit forms.
Optional hash prefix
/^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/ // # optional
/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})\b/ // extraction (no anchors, word boundary)Language-Specific Usage
JavaScript
const HEX = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
function isHexColor(str) {
return HEX.test(str);
}
isHexColor('#FFF'); // true
isHexColor('#FF5733'); // true
isHexColor('#GGGGGG'); // false
isHexColor('FF5733'); // false (no hash)
// Extract all hex colors from a CSS block
const cssText = 'color: #333; background: #FF573380; border: 1px solid #F0C';
cssText.match(/#([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{3})\b/g);
// → ['#333', '#FF573380', '#F0C']Python
import re
HEX = re.compile(r'^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$')
def is_hex_color(s: str) -> bool:
return bool(HEX.match(s))
is_hex_color('#FF5733') # True
is_hex_color('#F0C') # True
# Extract every hex color from a stylesheet
with open('theme.css') as f:
colors = re.findall(r'#[A-Fa-f0-9]{3,8}\b', f.read())
PHP
function isHexColor(string $s): bool {
return (bool) preg_match('/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/', $s);
}
// Normalize a hex color to lowercase 6-digit form
function normalizeHex(string $s): ?string {
if (!preg_match('/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/', $s, $m)) return null;
$hex = strtolower($m[1]);
if (strlen($hex) === 3) {
$hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2];
}
return '#' . $hex;
}Common Pitfalls
Alternation order matters
/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/ is subtly wrong when combined with certain engines and the g flag on non-anchored patterns — the shorter branch can match first and stop. Always put the LONGER alternative first: {6}|{3} or use a length range like {3,6}only where you don't care about 4- and 5-digit strings.
Missing the # is not the same as invalid
Some APIs accept hex codes without the leading #. If your data comes from a design token file or CSS custom property fallback, allow the optional hash with #?.
Case sensitivity trap
[A-F0-9] alone will reject lowercase codes like #ffffff. Always use [A-Fa-f0-9] or add the i flag.
Regex doesn't validate the color
#GG0000 fails the regex, but #800000passes even though it's a valid maroon. Regex validates the FORMAT, not the semantic color. If you need to reject "too dark" or "too close to another color", parse the hex to RGB and compare numerically.
Hex Color Cheatsheet
| Goal | Pattern | Matches |
|---|---|---|
| 6-digit strict | /^#[A-Fa-f0-9]{6}$/ | #FFFFFF, #FF5733 |
| 3 or 6 digit | /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/ | #FFF, #FFFFFF |
| All modern hex | /^#[A-Fa-f0-9]{3,8}$/ | #F0C, #FF573380 |
| Optional hash | /^#?[A-Fa-f0-9]{6}$/ | #FF5733, FF5733 |
| Extract from CSS | /#[A-Fa-f0-9]{3,8}\b/g | every hex in text |
Testing Your Hex Color Regex
Use the live Regex Tester above with these test strings:
- Match:
#FFF,#FF5733,#000000 - Match (with alpha):
#FF573380,#F0CA - Reject:
#FF57,#GGGGGG,#FF57333 - Reject:
rgb(255,87,51),red, empty string - Edge:
FF5733without hash — depends on your pattern
Common Mistakes When Writing Hex Color Regex
A few pitfalls trip up developers writing hex-color validators:
- Assuming exactly 6 digits.Modern CSS supports 3, 4, 6 and 8 digit hex. If you only validate 6, you'll silently reject legitimate shorthand like
#F0C. - Forgetting the alternation order. With unanchored patterns and the g flag, put longer alternatives first so
#FFFAAAmatches as 6 digits, not as 3 followed by trailing junk. - Only allowing uppercase.
[A-F0-9]rejects#ffffff. Use[A-Fa-f0-9]or add theiflag. - Forgetting the hash is a metacharacter in some contexts. In most regex flavours
#is literal, but in verbose/x-mode (Pythonre.VERBOSE, PHP/x)#introduces a comment. Escape or disable verbose mode. - Not handling
rgb(),hsl(), and named colors. If your input source is CSS, a hex-only regex will silently drop many valid colors. Consider a broader color-value regex, or preprocess the input to hex first.
Performance Notes
Hex color regexes are extremely fast. Character classes like [A-Fa-f0-9]compile to tight branch tables in every major engine (V8, PCRE, RE2, .NET), and there's no backtracking risk because the quantifiers are all bounded ({3}, {6}). Even validating millions of hex strings runs in single-digit milliseconds. The one thing that CAN slow you down is calling the regex inside a tight rendering loop — cache the compiled regex object outside the loop (Python re.compile, JS regex literal) rather than recompiling on every call.