Algorithms

Why JavaScript thinks 7 is an emoji

By Francesco Di Donato
August 9, 2026
5 minutes reading
The digit 7 beside three Unicode tests: Emoji true, Emoji Presentation false and RGI Emoji false

This is valid JavaScript:

/\p{Emoji}/u.test("7"); // true

Apparently, JavaScript thinks 7 is an emoji.

It gets better. The same test returns true for every ASCII digit, plus # and *. None of them has suddenly turned yellow or acquired a face. They still look like text.

This is not a regex bug. It is a correct answer to a question we probably did not mean to ask.

The regex never looked at the screen

The \p{...} syntax is called a Unicode property escape. It asks whether a code point belongs to a named set in the Unicode Character Database.

In this case, the set is literally called Emoji. Unicode Emoji defines an emoji character as a character with that property. ECMAScript exposes the same property to regular expressions.

So the regex does not inspect a glyph, a font or a screenshot. It looks up U+0037, the code point for 7, and finds Emoji=Yes.

That still leaves the awkward part. Why did Unicode put an ordinary digit there?

Seven is a component waiting for two more pieces

The answer is hiding in this version of seven:

7️⃣

That keycap is not one encoded character. It is a three-part sequence:

U+0037  DIGIT SEVEN
U+FE0F  VARIATION SELECTOR-16
U+20E3  COMBINING ENCLOSING KEYCAP

The first part supplies the symbol. The invisible variation selector requests emoji-style presentation. The final combining mark asks the renderer to put the symbol inside a keycap.

Unicode defines the same construction for every digit, # and *: [0-9#*] + U+FE0F + U+20E3. Its Emoji 17.0 data therefore marks those base characters as both Emoji and Emoji_Component.

7 belongs to the machinery used to build an emoji. That does not mean 7 alone should be drawn as one.

This is the same distinction that lets one family emoji have four different string lengths: a picture that looks atomic can be assembled from several encoded parts. Here, however, we are not counting the parts. We are asking what role each part is allowed to play.

Three properties, three different questions

A tempting fix is to replace Emoji with Emoji_Presentation:

/\p{Emoji_Presentation}/u.test("7"); // false

This property asks whether a character should appear as an emoji by default. It correctly excludes the ordinary digit.

Unfortunately, it also excludes this:

/\p{Emoji_Presentation}/u.test("❤️"); // false

The heart appears as an emoji because the full string contains U+FE0F, the emoji presentation selector. The base heart does not have default emoji presentation, and the selector itself does not have the Emoji_Presentation property. Testing the code points one at a time misses the instruction created by the sequence.

Current ECMAScript has a third tool for runtimes that support the v regex flag: Unicode properties of strings. Unlike ordinary character properties, these can recognize a sequence of several code points.

/^(?:\p{RGI_Emoji})$/v.test("7");  // false
/^(?:\p{RGI_Emoji})$/v.test("7️⃣"); // true
/^(?:\p{RGI_Emoji})$/v.test("❤️"); // true

RGI means Recommended for General Interchange. RGI_Emoji covers characters and complete sequences that Unicode recommends for broad cross-platform support. It recognizes the construction instead of merely noticing that one component has an emoji-related property.

These tests now answer three distinct questions:

TestWhat it asks about 7Result
\p{Emoji}Does this code point have Unicode’s Emoji property?true
\p{Emoji_Presentation}Does this code point default to emoji presentation?false
\p{RGI_Emoji}Is this complete string an RGI emoji?false

No row is contradicting another. We changed the question each time.

The wrong question leaves strange debris

This distinction matters as soon as the program does something with the match.

Suppose we try to remove emoji from text:

"Studio 7".replace(/\p{Emoji}/gu, "");
// "Studio "

The regex quietly deletes an ordinary digit. With a real keycap sequence, the result is stranger:

const remainder = "7️⃣".replace(/\p{Emoji}/gu, "");

[...remainder].map(character =>
  `U+${character.codePointAt(0).toString(16).toUpperCase()}`
);

// ["U+FE0F", "U+20E3"]

The remover deleted 7 because that code point has Emoji=Yes. It left the invisible presentation selector and the combining keycap mark because they do not. Depending on the font, the residue may be invisible, appear as a broken mark or attach itself somewhere unexpected.

The family from the previous article fails in the same way. Removing every Emoji code point deletes the people but leaves three Zero Width Joiners in the string. The output can look empty while still containing data.

For an emoji-only field, there is another independent trap. A bare .test() asks whether the input contains a match:

/\p{RGI_Emoji}/v.test("invoice 7️⃣ ready"); // true

If the product rule is that the entire value must consist of RGI emoji, the regex needs whole-string semantics too:

const emojiOnly = /^(?:\p{RGI_Emoji})+$/v;

emojiOnly.test("7");   // false
emojiOnly.test("7️⃣");  // true
emojiOnly.test("7️⃣❤️"); // true

That is a better implementation for that specific requirement. It is not a universal definition of everything a platform might display as an emoji. The RGI set is versioned, vendors still control the final glyph, and Unicode does not currently give RGI_Emoji a stability guarantee across future releases.

“Is this an emoji?” is still missing a requirement

JavaScript never decided that the glyph 7 looks like an emoji. It found a property attached to U+0037 because that code point participates in keycap emoji sequences.

The renderer sees a sequence and tries to draw a picture. The regex sees code points and registered strings. Those are different layers, with different definitions of success.

So “detect emoji” is not yet a complete task. Do we want code points that can participate in emoji, default emoji presentation, complete RGI sequences, or a field made exclusively from them?

Until that decision is explicit, true can be perfectly correct and still produce the wrong program.