TechEarl

How to Match a Hex Color Code with Regex

Match a hex color code with regex. 3-digit, 6-digit, and 8-digit (alpha) forms. JavaScript / Python / PHP examples, engine notes, common mistakes, a stripped-hash variant.

Ishan KarunaratneIshan Karunaratne⏱️ 8 min readUpdated
Match a hex color code with regex. 3-digit, 6-digit, and 8-digit (alpha) forms. Case-insensitive. JavaScript / Python / PHP examples, engine notes, common mistakes, test cases.

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):

code
^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$

With alpha support (8 or 4 hex digits also allowed):

code
^#?([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{3})$

Lowercase-only (rejects uppercase):

code
^#?([a-f0-9]{6}|[a-f0-9]{3})$

The practical pattern

code
^#?([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: both A-F and a-f are 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:

code
^#?([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:

code
^([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:

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:

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:

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.

EnginePractical patternRGB conversion
JavaScriptWorksBrowser: 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)Worksint(hex[0:2], 16) per channel. The colour package handles named colours too.
PHP (PCRE)Workshexdec(substr($hex, 0, 2)) per channel.
JavaWorksColor.decode(s) accepts #RRGGBB. For alpha, new Color(int, true) after manual parse.
.NETWorksColorTranslator.FromHtml(s) accepts #RRGGBB.
Go (RE2)Worksstrconv.ParseUint(s, 16, 32) for the parsed value.
Rust (regex crate)Worksu8::from_str_radix(s, 16) per channel.
RubyWorkss.to_i(16) for the integer; or use the color gem.
POSIX ERE (grep -E)WorksNone; 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:

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"); // null

The 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

InputPractical patternWith alpha
#ff0080MatchMatch
ff0080MatchMatch
#fffMatchMatch
#FFFMatchMatch
#ff0080ccNo matchMatch
#fff8No matchMatch
#1234No matchMatch
rgb(255,0,128)No matchNo match
#zzzNo matchNo match
#1234567No matchNo match (7 digits)

FAQ

See also

External reference: the CSS Color Module Level 4 spec defines the 4-digit and 8-digit alpha-channel formats. Test interactively at regex101.com.

TagsRegexHex ColorCSSColor ValidationRegular ExpressionsJavaScriptPythonPHP
Share
Ishan Karunaratne

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years across software, Linux systems, DevOps, and infrastructure — and a more recent focus on AI. Currently Chief Technology Officer at a tech startup in the healthcare space.

Keep reading

Related posts

Match a URL with regex. http/https schemes, protocol-relative URLs, ports, paths, query strings, fragments. JavaScript / Python / PHP examples, engine notes, parser alternative, common mistakes, test table.

How to Match a URL with Regex

Match a URL with regex. Covers http/https schemes, protocol-relative URLs, ports, paths, query strings, fragments, runnable JavaScript / Python / PHP, engine notes, and the URL parser alternative.

Match an email address with regex. Practical pattern, strict RFC 5321 pattern, JavaScript / Python / PHP examples, edge cases, engine compatibility, common mistakes, and a test table.

How to Match an Email Address with Regex

Match an email address with regex. The practical pattern, the strict RFC 5321 pattern, examples in JavaScript, Python, and PHP, edge cases, engine compatibility, common mistakes, and a validation test table.

Match a domain name with regex. Basic labels, RFC 1035 length rules, subdomains, IDN punycode, trailing-dot form, JavaScript / Python / PHP examples, engine notes, and common mistakes.

How to Match a Domain Name with Regex

Match a domain name with regex. Basic labels, RFC 1035 length rules, subdomains, IDN punycode, trailing-dot form, JavaScript / Python / PHP examples, engine notes, and common mistakes.