The shortest regex for a hex color code: ^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$. It accepts both the 3-character shorthand (#fff) and the full 6-character form (#ffffff), with or without the leading hash, in upper or lower case. For modern CSS that includes an alpha channel (#ff0080cc), add the 8-digit case. Below I walk all three, with runnable code in JavaScript, Python, and PHP, engine notes, common mistakes, and the stripped-hash variant for when the hash has already been removed.
The reason this comes up so often is that CSS, design tools, and JSON colour configs all use the same #RRGGBB format but differ on whether the hash is included, whether shorthand is allowed, and whether alpha is supported. One pattern covers the common cases.
Quick reference
The practical pattern (3 or 6 hex digits, optional hash):
^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$
With alpha support (8 or 4 hex digits also allowed):
^#?([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{3})$
Lowercase-only (rejects uppercase):
^#?([a-f0-9]{6}|[a-f0-9]{3})$
The practical pattern
^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$
Reading left to right:
^and$anchor to the full string.#?is an optional hash (CSS forms include it; some design-tool configs omit it).[A-Fa-f0-9]matches one hex digit. Case-insensitive: bothA-Fanda-fare valid.{6}|{3}is the alternation: either six digits (full) or three digits (shorthand).
Order matters here: {6} must come before {3} in the alternation, otherwise the regex matches the first three digits of a six-digit string and leaves the rest unmatched. Anchors save us in this exact case, but in unanchored patterns the order would change which match is returned.
With alpha channel (8-digit)
Modern CSS supports #RRGGBBAA and #RGBA for colours with transparency:
^#?([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{3})$
{8}for#ff0080cc(red, green, blue, alpha).{4}for#f08c(shorthand with alpha).
The 4 and 3 shorthand forms expand by doubling each digit: #f08c becomes #ff0088cc, #f80 becomes #ff8800.
Without the leading hash
If the input has already had the hash stripped (or never had one), make the # non-optional or drop it:
^([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$
Useful in code paths where you've already separated the hash from the value (const hex = input.startsWith("#") ? input.slice(1) : input).
Examples in JavaScript, Python, and PHP
JavaScript:
const hexColor = /^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/;
const hexWithAlpha = /^#?([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{3})$/;
function isValidHex(input) {
return hexColor.test(input);
}
isValidHex("#ff0080"); // true
isValidHex("ff0080"); // true (no hash)
isValidHex("#fff"); // true (shorthand)
isValidHex("FFFF"); // false (4 digits without alpha pattern)
isValidHex("#zzz"); // false (z is not hex)Python:
import re
HEX_COLOR = re.compile(r"^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$")
HEX_WITH_ALPHA = re.compile(r"^#?([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{3})$")
def is_valid_hex(value: str) -> bool:
return bool(HEX_COLOR.match(value))
is_valid_hex("#a1b2c3") # True
is_valid_hex("abc") # True (shorthand without hash)
is_valid_hex("#1234") # False (4 digits, no alpha pattern)PHP:
function isValidHex(string $value): bool {
return (bool) preg_match('/^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/', $value);
}
isValidHex("#ff0080"); // true
isValidHex("#f08"); // true (shorthand)
isValidHex("#FF0080"); // true (uppercase)Engine compatibility
The hex color pattern uses only universal features (anchors, character classes, alternation). It runs unmodified everywhere. The per-engine notes are about colour parsing helpers when you want to convert the hex to RGB after the regex passes.
| Engine | Practical pattern | RGB conversion |
|---|---|---|
| JavaScript | Works | Browser: CSS.supports('color', s) validates; for parsing, render to canvas and read getImageData. Or split the hex into 2-char pairs and parseInt(c, 16). |
Python (re) | Works | int(hex[0:2], 16) per channel. The colour package handles named colours too. |
| PHP (PCRE) | Works | hexdec(substr($hex, 0, 2)) per channel. |
| Java | Works | Color.decode(s) accepts #RRGGBB. For alpha, new Color(int, true) after manual parse. |
| .NET | Works | ColorTranslator.FromHtml(s) accepts #RRGGBB. |
| Go (RE2) | Works | strconv.ParseUint(s, 16, 32) for the parsed value. |
Rust (regex crate) | Works | u8::from_str_radix(s, 16) per channel. |
| Ruby | Works | s.to_i(16) for the integer; or use the color gem. |
POSIX ERE (grep -E) | Works | None; pipe to awk for parsing |
All major engines support the pattern as-is, including the alpha variant. Hex colour validation is one of the cleanest cross-engine regex use cases.
Converting between formats
Once the regex passes, you usually want to normalise the value. JavaScript:
function normalizeHex(input) {
if (!hexColor.test(input)) return null;
let hex = input.startsWith("#") ? input.slice(1) : input;
if (hex.length === 3) {
// Expand shorthand: fff -> ffffff
hex = hex.split("").map(c => c + c).join("");
}
return "#" + hex.toLowerCase();
}
normalizeHex("#FFF"); // '#ffffff'
normalizeHex("aB1"); // '#aabb11'
normalizeHex("#abcdef"); // '#abcdef'
normalizeHex("not-a-color"); // nullThe function does three useful things: validates, expands shorthand to full, and lowercases for consistency. Most colour-picker UIs store the long lowercase form internally.
Common mistakes
The bugs I see most often.
Putting {3} before {6} in the alternation. Without anchors, the engine matches the first three characters of a six-character string and returns early. Always list the longer alternative first.
Forgetting the anchors. [A-Fa-f0-9]{6} matches the first six hex digits of not-a-color-abcdef-extra because no ^ or $ pins the boundaries. Always anchor for validation.
Using \w instead of [A-Fa-f0-9]. \w includes letters beyond A-F (and underscore), so #GHIJKL slips through. Always use the explicit hex character class.
Mixing the alpha pattern with the no-alpha pattern. A pattern that accepts 4 digits but not 8 (or vice versa) gives weird behaviour: #abcd matches but #abcdef12 doesn't. Either pick the no-alpha pattern or the full alpha pattern; don't half-implement it.
Not handling the missing hash. A pattern that requires # rejects fff and ffffff that you might get from a JSON config or a CSV. Use #? to accept both.
Treating Unicode confusables as hex. #fff (full-width letters) doesn't match the ASCII [A-Fa-f0-9], which is usually the right behaviour. Just be aware that a user pasting from a Unicode editor might hit this.
Test cases
| Input | Practical pattern | With alpha |
|---|---|---|
#ff0080 | Match | Match |
ff0080 | Match | Match |
#fff | Match | Match |
#FFF | Match | Match |
#ff0080cc | No match | Match |
#fff8 | No match | Match |
#1234 | No match | Match |
rgb(255,0,128) | No match | No match |
#zzz | No match | No match |
#1234567 | No match | No match (7 digits) |
FAQ
See also
- How to Match Numbers with Regex: the
rgb(0,255,128)cousin where channel values are decimal integers in 0-255 range - How to Match a URL with Regex: asset URLs that often sit alongside colour values in theme JSON
- Regex Anchors: why
^and$matter for the hex pattern's alternation order - Regex Word Boundaries: the
\B#trick for extracting hex colours from CSS files - Regex Lookaheads and Lookbehinds: composing extra constraints like "preceded by a CSS property"
- Regex Capturing Groups and Backreferences: pull each channel pair out separately
- Regex Cheat Sheet: the wider syntax and engine compatibility reference
External reference: the CSS Color Module Level 4 spec defines the 4-digit and 8-digit alpha-channel formats. Test interactively at regex101.com.





