Skip to main content
Best Emojis

Home > Blog > Technology

Technology
emoji regex
javascript
unicode

Which Emoji Regex Actually Works? We Tested 7 Patterns on All 3,963 Emoji

By BestEmojis TeamยทOctober 22, 2026ยท11 min read
Which Emoji Regex Actually Works? We Tested 7 Patterns on All 3,963 Emoji

Only one of the seven emoji patterns we tested matched all 3,963 emoji in Unicode 18.0 as whole units: the emoji-regex package at version 11.0.0. The built-in \p{RGI_Emoji} came second with 3,944, losing the 19 newest because our Node runtime only knows Unicode 17.0. \p{Extended_Pictographic} never sees 259 flags or 12 keycaps, and \p{Emoji} treats every digit as an emoji. For splitting text, skip regex and use Intl.Segmenter.

Below are the full results, the exact misses for each pattern, and which one to reach for depending on whether you need to detect, split, strip or count.

How we ran the test

The test list is Unicode's own emoji-test.txt for version 18.0, dated 2026-04-30, which names 3,963 fully-qualified emoji. That is every emoji a keyboard should produce, including all 2,040 skin tone variants, 1,614 sequences joined with a zero width joiner, 259 country flags, 3 subdivision flags and 12 keycaps.

Every pattern ran on Node v25.2.1, which ships ICU 78.1 and Unicode 17.0 data. Our build script, scripts/build-dev-emoji-stats.js, first recorded these figures on 2026-10-20, and we reran the wider seven-pattern comparison on the same Node version on 2026-10-22. That runtime detail is not a footnote. The Unicode property escapes in a JavaScript regex are only as current as the engine running them, so the same line of code gives different answers on Node 20, Node 25 and whatever ships next year.

We scored two things per pattern. Detects means the regex found something inside the emoji at all. Whole match means a global match returned exactly one hit and that hit was the complete emoji, which is what you need before you can split, replace or count reliably.

The scoreboard, pattern by pattern

  • /\p{Emoji}/gu: detects 3,954, whole match 1,184. Also matches #, *, every digit from 0 to 9, plus ยฉ and ยฎ.
  • /\p{Emoji_Presentation}/gu: detects 3,725, whole match 1,184. Misses 238 entirely, including 207 emoji that rely on the U+FE0F style selector, such as โค๏ธ.
  • /\p{Extended_Pictographic}/gu: detects 3,692, whole match 1,193. Misses all 259 country flags and all 12 keycaps.
  • A hand-written code point range (the [โ€€-ใŒ€] plus surrogate-range style still pasted from old answers): detects 3,963, whole match 1,193, and strips ordinary punctuation.
  • /\p{RGI_Emoji}/gv: detects 3,954, whole match 3,944. Misses the 19 Emoji 18.0 entries on this runtime.
  • emoji-regex 9.2.2 (built from Unicode 13.0 data): detects 3,883, whole match 3,512.
  • emoji-regex 11.0.0 (built from Unicode 18.0 data, published to npm 2026-09-15): detects 3,963, whole match 3,963.

And outside the regex family, Intl.Segmenter with grapheme granularity returned exactly one segment for all 3,963, including the 19 emoji our runtime has never heard of. That is the single most useful result in this test, and we come back to why below.

Why \p{Emoji} is the trap everybody falls into

The name promises something it does not deliver. The Emoji property marks every character that can be shown as an emoji, and the keycap bases are on that list because #๏ธโƒฃ starts with a plain #. So a filter like text.replace(/\p{Emoji}/gu, '') turns "Order #1" into "Order " and wipes phone numbers, prices and dates.

It is wrong in the other direction too. It matched only 1,184 emoji as whole units, because it sees code points, not sequences. A family is matched as three or four separate people, and in our strip test 1,855 of the 3,963 emoji left something behind: 1,614 of them a stray zero width joiner, 1,180 a dangling U+FE0F. Those leftovers are invisible, which is why this bug survives code review.

If you have \p{Emoji} in production right now, check two things: whether anything numeric passes through it, and whether your cleaned strings still contain U+200D.

Extended_Pictographic: right for detection, blind to flags

\p{Extended_Pictographic} fixes the digit problem. Only ยฉ and ยฎ from the whole Latin-1 range match it, though โ„ข and some arrows further up do too. That makes it the pattern most people should use for a quick "does this message contain an emoji" check, with one large caveat.

Flags and keycaps contain no pictographic code point at all. A flag such as ๐Ÿ‡ฎ๐Ÿ‡ณ is two regional indicator letters, and a keycap is a digit, a style selector and U+20E3. So \p{Extended_Pictographic} walks straight past 271 emoji, and a moderation filter built on it lets every flag through. Our flag rendering test shows how those letter pairs behave on screen.

The fix for detection is a three-part alternation, which detected all 3,963 in our run:

/\p{Extended_Pictographic}|\p{Regional_Indicator}|โƒฃ/u

It still treats ยฉ, ยฎ, โ„ข and a handful of arrows as emoji even in their plain text form, because Unicode lists them as pictographic. Decide whether that matters for your use before shipping it.

The surprise: Extended_Pictographic already knows about future emoji

Here is the result we did not expect. On a runtime with Unicode 17.0 data, \p{Extended_Pictographic} matched all 19 Emoji 18.0 entries, including the brand new ๐Ÿซน leftwards thumb sign and ๐Ÿซ pickle. \p{Emoji} and \p{RGI_Emoji} did not.

The reason is in Unicode's own data file. The Unicode 17.0 emoji-data.txt lists 39 lines of reserved code point ranges under Extended_Pictographic, among them 1FAF9..1FAFF, 1FA8B..1FA8D and 1F6D9..1F6DB. Those are exactly the slots the new thumbs, the meteor, the eraser, the net and the lighthouse later landed in. Unicode's emoji specification, UTS #51 (unicode.org/reports/tr51), explains why: unassigned code points in blocks "intended for future allocation of emoji characters" get the property early, "so that possible future emoji ZWJ sequences will not break grapheme clusters, words, or lines."

That makes Extended_Pictographic the most future-proof of the built-in properties for detection. It does not make it good at splitting, since it still sees code points rather than sequences.

\p{RGI_Emoji}: nearly perfect, and quietly out of date

The v flag, added in ES2024, lets a regex use properties of strings. \p{RGI_Emoji} is one of them: it matches the complete recommended sequence, so a family, a flag, a keycap and a toned hand each come back as one hit. On our run it matched 3,944 of 3,963 whole, with zero false positives in Latin-1. No digits, no ยฉ.

The 19 misses are every Emoji 18.0 entry, the set covered on our Unicode 18.0 page. Nine are new single characters the engine simply does not recognise. The other ten are skin tone variants of the two new thumbs, and they fail in an uglier way. The skin tone modifier on its own is a valid RGI emoji, so the regex matches just the tone. Strip ๐Ÿซน๐Ÿฝ with \p{RGI_Emoji} and you are left with a bare ๐Ÿซน, untoned, which many devices still cannot draw.

None of that is a bug in the regex. The property list is compiled into V8 from whatever Unicode version the engine shipped with. Our Emoji 18.0 rollout tracker shows the same lag on the artwork side: vendors draw new emoji months after the standard publishes them, and engines catch up on their own schedule too.

The emoji-regex package, and the copy already in your node_modules

emoji-regex generates its pattern at build time from a pinned Unicode release, which makes it deterministic. Version 11.0.0 lists @unicode/unicode-18.0.0 in its dev dependencies and matched all 3,963 emoji whole, the only pattern in this test to do so. It also left zero residue when used to strip.

It is opinionated about text-style symbols. Its README says it matches "textual representations of emoji" too, so a plain ยฉ or โ„ข in a footer disappears when you strip with it. For an image-replacement pipeline that is what you want. For a comment sanitiser it may not be.

Now the uncomfortable part. Running npm ls emoji-regex on this site's own repository turns up version 9.2.2, pulled in by eslint-plugin-jsx-a11y, and its package file pins Unicode 13.0. Tested on the same list it failed to whole-match 451 emoji, and those 451 are every single emoji from Emoji 14.0 onward:

  • Emoji 14.0: 112
  • Emoji 15.0: 31
  • Emoji 15.1: 118
  • Emoji 16.0: 8
  • Emoji 17.0: 163
  • Emoji 18.0: 19

A transitive dependency like that is easy to import by accident because your editor autocompletes it. Pin the version you depend on directly, and check which copy your bundler actually resolved. The Unicode version history shows how many emoji each of those releases added.

Hand-written ranges belong in the bin

The code point range pattern looks thorough because it detected every emoji. That is only because it catches far too much. The range U+2000 to U+3300 covers general punctuation, currency, arrows and math. Run on a short test sentence, it deleted an en dash, both curly quotes, an ellipsis, the euro sign, โ„ข, an arrow and a check mark, while leaving a dangling U+FE0F behind in 1,180 emoji.

It even matches the zero width joiner itself, U+200D, so it counts a three-person family as five things. There is no version of this pattern worth patching. Replace it.

Counting emoji: ten, five or four?

We ran each pattern over one realistic message, Great job ๐Ÿ‘๐Ÿฝ team! ๐Ÿ‡ฏ๐Ÿ‡ต trip โค๏ธ #1 ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง, which a person would say contains four emoji.

  • \p{Emoji} counted 10: the thumb and its tone separately, both flag letters, the heart, the #, the 1, and each family member.
  • The range pattern also counted 10, by a different route: it counted both joiners.
  • \p{Extended_Pictographic} counted 5: it lost the flag and split the family into three.
  • \p{RGI_Emoji} and emoji-regex 11.0.0 counted 4.

If your product shows "you used 3 of 5 reactions" or limits emoji per message, only the last two give a number users will agree with, and only while the runtime is current.

Splitting text: let the segmenter do it

For splitting, a regex is the wrong tool. Intl.Segmenter breaks text into grapheme clusters, the units a reader perceives, and it returned exactly one cluster for all 3,963 emoji on a runtime that does not know the newest 19.

It manages that because grapheme rules are structural. A cluster continues across skin tone modifiers, style selectors and joiners, and regional indicators pair up two at a time, none of which needs a list of named emoji. The segmenter only needs Extended_Pictographic, which, as above, already covers the reserved slots.

Combine the two for a robust "find every emoji" helper:

const seg = new Intl.Segmenter('en', { granularity: 'grapheme' });
const isEmoji = /\p{Extended_Pictographic}|\p{Regional_Indicator}|โƒฃ/u;
const emojis = [...seg.segment(text)].map(s => s.segment).filter(g => isEmoji.test(g));

On our test message that returned ๐Ÿ‘๐Ÿฝ, ๐Ÿ‡ฏ๐Ÿ‡ต, โค๏ธ and ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘ง as four intact strings, and it kept ๐Ÿซน๐Ÿฝ whole where \p{RGI_Emoji} broke it. It also returned ยฉ from a copyright line, the same caveat as before. To see what each of those strings is made of, paste one into our emoji to code point converter.

Pick the pattern by the job

  • Detect ("does this contain an emoji?"): \p{Extended_Pictographic}|\p{Regional_Indicator}|โƒฃ. It catches flags, keycaps and emoji newer than your runtime.
  • Split (render, highlight, wrap each emoji): Intl.Segmenter, then filter clusters with the detect pattern.
  • Strip (sanitise a username or slug): emoji-regex at a version that matches the newest Unicode release, then a final pass removing any leftover U+200D, U+FE0F and U+20E3 so nothing invisible survives.
  • Count: the length of the split result above. \p{RGI_Emoji} also works on a current engine, but it miscounts brand new emoji.
  • Validate ("is this string exactly one emoji?"): /^\p{RGI_Emoji}$/v, accepting that it will reject the newest release until your runtime updates.

Never use \p{Emoji} on its own for any of these. Its legitimate job is checking whether a single known character has an emoji form, and almost nobody needs that.

Re-running the test when Unicode moves

Each figure above is a snapshot of one runtime against one Unicode release. When Emoji 19.0 lands, \p{RGI_Emoji} on today's Node will miss those too, emoji-regex 11 will miss them, and the segmenter will very likely still cope.

The script behind our developer reference is short enough to copy. It downloads emoji-test.txt for a chosen version, keeps the fully-qualified rows, and records process.versions.unicode next to every result, so a reader can tell which engine produced which number. Run something similar in your CI whenever you bump Node or a regex dependency. The cheapest regression test for an emoji filter is the official list of every emoji, and Unicode publishes it for free.

For hand checks, the full emoji list groups all 1,923 base designs, the ZWJ sequence table splits every joined design into its parts, and the skin tone page has every toned variant in one place. Those three cover the sequences regexes most often break.

If the joiner behaviour above was new, our guide to ZWJ sequences explains how families and professions are glued together. The keycap guide covers why # and the digits carry the emoji property in the first place. For the 19 emoji every built-in pattern struggled with, see our rundown of the Emoji 18.0 additions. And once you have found the emoji, our piece on storing emoji in a database covers what happens when you try to save them.

Frequently Asked Questions

Which regex matches every emoji in JavaScript?+

In our test on Node 25 only the emoji-regex package at version 11.0.0 matched all 3,963 Unicode 18.0 emoji as whole sequences. The built-in /\p{RGI_Emoji}/v matched 3,944, missing the 19 Emoji 18.0 entries because the engine's Unicode data stopped at 17.0.

Why does \p{Extended_Pictographic} not match flag emoji?+

Flags are built from two regional indicator letters, and neither letter is a pictographic character. Keycaps are a digit plus U+20E3, also non-pictographic. Together that is 271 emoji the property never sees, so add \p{Regional_Indicator} and \u20E3 to the pattern.

Why did my emoji filter delete numbers and hash signs?+

You almost certainly used \p{Emoji}. That property includes 0 to 9, #, * and the Latin-1 symbols ยฉ and ยฎ, because each of them can start an emoji keycap or has an emoji form. Use \p{Extended_Pictographic} or \p{RGI_Emoji} instead.

Can a regex match emoji that are newer than my Node version?+

\p{RGI_Emoji} cannot, since its list is compiled into the engine. \p{Extended_Pictographic} often can, because Unicode assigns that property to reserved code points ahead of time. On Unicode 17.0 data it matched all 19 Emoji 18.0 entries in our test.

Is Intl.Segmenter better than a regex for emoji?+

For splitting text into emoji, yes. It returned one grapheme for each of the 3,963 emoji we tested, newest included, because grapheme rules follow joiners, modifiers and flag pairs structurally. It does not tell you which segments are emoji, so pair it with a detection pattern.

How often should I re-test my emoji regex?+

Whenever you upgrade Node, a browser baseline or an emoji library, and after each yearly Unicode release. Download emoji-test.txt, run your pattern over the fully-qualified rows, and log process.versions.unicode next to the result so the numbers stay traceable.

About BestEmojis Team

The Best Emojis team is dedicated to helping you find the perfect emojis for every occasion. We provide comprehensive guides, tips, and a vast collection of free emojis for all your communication needs.

More from Our Blog