Why Emoji Regex is Harder Than It Looks
Modern emoji are not single code points. A visible symbol like 👨👩👧is actually five code points joined by zero-width joiners (U+200D). A regex that matches "one emoji" with a naive pattern will report three matches for that single visible glyph. Flags like 🇮🇳 are regional-indicator pairs (two code points). Keycaps like 1️⃣ are three code points (digit + VS16 + U+20E3). Any serious emoji-handling code has to understand these sequences, not just raw code points.
The Modern Pattern (ES2018+)
ECMAScript 2018 added Unicode property escapes, which turn emoji detection into a one-liner:
/\p{Extended_Pictographic}/gu
// Flags required:
// u — enable full Unicode mode (property escapes, code-point iteration)
// g — find all matches, not just the first
// Input: "Hi 👋 team! 🎉"
// Matches: ["👋", "🎉"] — two matches, correctly skipping ASCIIWhich Unicode Property Should You Use?
Four properties can be used to match emoji — and they do different things:
\p{Emoji}— matches digits 0-9 and#,*because they're emoji_presentation base characters. False positives for most use cases.\p{Emoji_Presentation}— matches only characters that render in color by default (👋 yes, ✈ no — airplane is text-presentation unless followed by VS16).\p{Extended_Pictographic}— the Unicode property specifically designed for "things that look like emoji in text". This is the right choice for counting, stripping, or detecting.\p{RGI_Emoji}— only in the new /v flag. Matches every officially-recommended-for-general-interchange emoji sequence as a single match, including ZWJ sequences.
Handling ZWJ Sequences
The plain \p{Extended_Pictographic} matches each code point individually. For 👨👩👧that's 3 matches, not 1. If you're stripping emoji that happens to work (you remove all three parts), but if you're counting it undercounts. The ZWJ-aware pattern groups them:
// Match a base emoji optionally followed by ZWJ + more emoji
const EMOJI_SEQ = /\p{Extended_Pictographic}(\u{200D}\p{Extended_Pictographic})*/gu;
const text = "Family 👨👩👧 chat 💬 — 3 members 🧑🏽🤝🧑🏻";
const matches = [...text.matchAll(EMOJI_SEQ)];
// matches.length === 3 (not 7)
// matches[0][0] === "👨👩👧"
// matches[1][0] === "💬"
// matches[2][0] === "🧑🏽🤝🧑🏻"Language-Specific Usage
JavaScript / TypeScript
const EMOJI = /\p{Extended_Pictographic}(\u{200D}\p{Extended_Pictographic})*/gu;
// Count visible emoji
const countEmoji = (s) => (s.match(EMOJI) || []).length;
countEmoji("Hi 👋 and 👨👩👧"); // 2
// Strip all emoji + leftover VS16
const stripEmoji = (s) =>
s.replace(EMOJI, "").replace(/\uFE0F/g, "").trim();
stripEmoji("Build passed ✅ 🎉"); // "Build passed"
// Has at least one emoji?
const hasEmoji = (s) => EMOJI.test(s); // Reset lastIndex after usePython 3.7+
import re
# Python's re module does NOT support \p{...} property escapes.
# Options:
# 1. Use the third-party 'regex' module (full Unicode support)
# 2. Use an explicit Unicode range
# Option 1 (recommended)
import regex
EMOJI = regex.compile(r'\p{Extended_Pictographic}(\u200D\p{Extended_Pictographic})*')
EMOJI.findall("Family 👨👩👧 chat 💬") # ['👨👩👧', '💬']
# Option 2 (stdlib only — approximate)
EMOJI_RANGE = re.compile(
"["
"\U0001F300-\U0001FAFF" # most emoji planes
"\U00002600-\U000027BF" # misc symbols + dingbats
"\U0001F900-\U0001F9FF" # supplemental symbols
"\u200D\uFE0F"
"]+", flags=re.UNICODE)
EMOJI_RANGE.findall("Hi 👋 team")Go
package main
import (
"regexp"
"fmt"
)
// Go's regexp uses RE2 which supports \p{} but NOT the full emoji
// property set. Use \p{So} (symbol-other) + manual ranges.
var emojiRe = regexp.MustCompile(`[\p{So}\p{Cn}]|[\x{1F300}-\x{1FAFF}]|[\x{200D}\x{FE0F}]+`)
func stripEmoji(s string) string {
return emojiRe.ReplaceAllString(s, "")
}
fmt.Println(stripEmoji("Build 🚀 shipped")) // "Build shipped"PHP 7.3+
$text = "Hi 👋 team 🎉";
// PCRE supports \p{...} with /u flag
preg_match_all('/\p{Extended_Pictographic}(\x{200D}\p{Extended_Pictographic})*/u', $text, $m);
print_r($m[0]); // ['👋', '🎉']
// Strip emoji
echo preg_replace('/\p{Extended_Pictographic}+/u', '', $text);
// "Hi team "Common Pitfalls
Forgetting the /u flag
Without /u, JavaScript throws SyntaxError: Invalid regular expressionat parse time. The error is caught early, but many developers mistakenly add /gonly and wonder why the pattern "doesn't work." Always use /gu as the minimum flag set for emoji regex.
Counting UTF-16 code units instead of emoji
"👨👩👧".length returns 8in JavaScript — not 1. Strings are UTF-16, each surrogate pair counts as 2, and the two ZWJs count as 2 more. If you're validating length for an input field, use [...str].length for code-point count or a ZWJ-aware regex for visible-emoji count.
Dangling variation selectors
After stripping emoji with \p{Extended_Pictographic} alone, you can leave orphan U+FE0F (VS16) characters. These are invisible but corrupt downstream Unicode-aware equality checks. Always chain.replace(/\uFE0F/g, "") after your main emoji strip.
Flag emoji aren't Extended_Pictographic
🇮🇳 (India flag) is two regional-indicator code points (🇮 + 🇳). Each is matched by\p{Regional_Indicator}, not by Extended_Pictographic. For a complete emoji regex, union the properties:/[\p{Extended_Pictographic}\p{Regional_Indicator}]/gu.
The New /v Flag (ES2024)
The /v flag (shipped in Chrome 112+, Node 20+, Safari 17+) adds set notation and proper emoji sequence matching. Instead of hand-crafting ZWJ-aware patterns, you can write:
// Modern engines: match any RGI emoji as a single hit
const EMOJI = /[\p{RGI_Emoji}]/gv;
const text = "👨👩👧 and 🧑🏽💻 chat 💬";
[...text.matchAll(EMOJI)].map(m => m[0]);
// ['👨👩👧', '🧑🏽💻', '💬']
// Set difference: emoji but not flags
const NO_FLAGS = /[\p{RGI_Emoji}--\p{Regional_Indicator}]/gv;Emoji Regex Cheatsheet
| Goal | Pattern | Notes |
|---|---|---|
| Any emoji (basic) | /\p{Extended_Pictographic}/gu | Splits ZWJ sequences |
| Any emoji (ZWJ-aware) | /\p{Extended_Pictographic}(\u{200D}\p{Extended_Pictographic})*/gu | Counts visible glyphs |
| Modern /v flag | /[\p{RGI_Emoji}]/gv | Chrome 112+, Node 20+ |
| Flags only | /\p{Regional_Indicator}{2}/gu | Pairs of regional-indicator |
| Keycap sequence | /[0-9#*]\uFE0F?\u{20E3}/gu | 1️⃣ 2️⃣ 3️⃣ etc. |
Testing Your Emoji Regex
Use the live Regex Tester with this canonical test block:
Hi 👋 team!
🎉 Build passed ✅ by 👨💻 Alex.
Flags: 🇮🇳 🇺🇸 🇯🇵
Keycaps: 1️⃣ 2️⃣ 3️⃣
Family: 👨👩👧👦 (one visible emoji, 7 code points)
Skin tone: 🧑🏽🤝🧑🏻
Patterns to try:
/\p{Extended_Pictographic}/gu — 14+ matches (splits ZWJ)
/\p{Extended_Pictographic}(\u{200D}\p{Extended_Pictographic})*/gu — 9 matches (visible glyphs)
/\p{Regional_Indicator}{2}/gu — 3 matches (flags only)Performance Notes
Unicode property escapes compile into optimised bytecode on every modern engine — V8 and SpiderMonkey both bake the full code-point table into the regex at compile time, so each character comparison is a single table lookup. Running the ZWJ-aware pattern over a megabyte of mixed text takes a few milliseconds. The ZWJ loop (\u{200D}\p{Extended_Pictographic})*cannot backtrack catastrophically because each iteration consumes at least two code points.