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 Karunaratne⏱️ 8 min readUpdated
Share thisCopied
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

^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$. Optional hash, then either 6 or 3 hex digits. The order of the alternation matters: {6} must come before {3} so that #ffffff matches the long form, not the first three characters as a shorthand.

Yes, because the character class [A-Fa-f0-9] includes both upper and lower case letters. You don't need the i flag to make it case-insensitive: the explicit ranges handle both cases.

If you want to reject mixed case (require all-lowercase, for example), use [a-f0-9] instead.

Add 8-digit and 4-digit alternatives to the pattern: ^#?([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{3})$. The 8-digit form is RRGGBBAA; the 4-digit form is RGBA shorthand.

Modern CSS (Color Module Level 4) and most design tools support these.

Yes but the patterns are longer because of the comma-separated values and parentheses: ^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$ for basic rgb().

For real CSS colour parsing, prefer a dedicated colour library. The regex catches the format but not whether each channel is in the valid 0-255 range.

Because the alternation only includes 6 and 3 as valid digit counts. 2 digits matches nothing, 4 digits matches nothing (in the no-alpha pattern), 5 digits matches nothing.

Hex colours are always either 3 (shorthand), 6 (full), 4 (shorthand with alpha), or 8 (full with alpha). Other lengths are not valid CSS.

Drop the anchors and add a word boundary to avoid matching inside identifiers: \B#([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{3})\b. The \B before # means the # is at a non-word-boundary (because # is itself non-word, the boundary is "always there" or "never there" depending on the surrounding character).

For a quick scan, plain #[A-Fa-f0-9]{3,8} over the file is usually good enough.

Only with the alpha variant. The no-alpha pattern rejects 4-digit forms. The alpha variant ^#?([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{3})$ accepts it.

The 4-digit form expands to 8 digits the same way 3-digit expands to 6: each character is doubled. #f08c becomes #ff0088cc.

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

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Software Systems Architect · Senior Software Engineer · Engineering Leadership

Software systems architect and senior software engineer with more than two decades designing, building, and running production software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Now a CTO, though what I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

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.

Use grep -w to match a whole word instead of a substring. What grep counts as a word boundary, the \b and \< \> regex equivalents, -x for whole-line match, and BSD vs GNU differences.

How to Match a Whole Word with grep -w

grep cat also matches category, concatenate, and scatter. grep -w cat matches only the standalone word. The whole-word flag, what grep counts as a word boundary, the regex equivalents with \b and \< \>, the stricter -x whole-line cousin, and the BSD vs GNU differences that bite on macOS.