The Tiny Unicode Gotcha Behind Your Emoji Tests Failing: U+FE0F

(Last modified: )☕️️ 5 mins read

I had one of those "this makes no sense" test failures today 😡:

- Expected: `☕☕`
- Received: `☕️☕️`

Both looked identical in my editor a few months ago, but they are different Unicode sequences and I see them differently now (different font, or different VS Code settings?):

  • -> U+2615
  • ☕️ -> U+2615 U+FE0F

Note: you may or may not see them differently in your device as you read this post. But bear with me, I'll explain why this happens.

That extra U+FE0F is Variation Selector-16, which requests emoji presentation. In visual terms, they often look the same. In string comparison terms, they are not equal.

Now I understand why in the emoji picker we can select the "same emoji" in different styles... 🤯

What Is U+FE0F?

U+FE0F is Variation Selector-16 (VS16).

Its purpose is simple:

It forces a character to render in emoji presentation instead of text presentation.

Many Unicode characters — like hearts, airplanes, or check marks — can be displayed either:

  • As plain text (monochrome, typographic style)
  • As colorful emoji

U+FE0F tells the renderer: "Make this the emoji version."

A Concrete Example. Take the red heart. Base character U+2764 (❤):

U+2764  (❤)

Depending on the platform, this might render as a simple black heart. Now add U+FE0F:

U+2764 U+FE0F  →  ❤️

They look almost identical (depending on the font, the OS, the program running it, etc.).

But they are different strings:

  • -> U+2764
  • ❤️ -> U+2764 U+FE0F

This is why your tests fail.

'❤' !== '❤️'

Go ahead, test it in the console:

'❤' !== '❤️'
console.log('\u2764') // "❤"
// Emoji version:
console.log('\u2764\uFE0F') // "❤️"
// Or with "code point escape" syntax:
console.log('\u{2764}\u{FE0F}') // "❤️"

Other Examples

Here are some examples of characters that can be displayed as both text and emoji:

  • ✈ / ✈️ (airplane)
  • ⌚ / ⌚️ (watch)
  • ⌛ / ⌛️ (hourglass)
  • ☎ / ☎️ (telephone)
  • ✉ / ✉️ (envelope)
  • ✂ / ✂️ (scissors)
  • ☀ / ☀️ (sun)
  • ☁ / ☁️ (cloud)
  • ☔ / ☔️ (umbrella)
  • ☕ / ☕️ (coffee)
  • ⚽ / ⚽️ (soccer ball)
  • ⭐ / ⭐️ (star)
  • ♥ / ♥️ (heart suit)
  • ♠ / ♠️ (spade suit)
  • ♣ / ♣️ (club suit)
  • ♦ / ♦️ (diamond suit)
  • ⚠ / ⚠️ (warning sign)

Two more important ones:

  • ©, ®, can also take U+FE0F (©️, ®️, ™️) depending on platform/font behavior.
  • Keycaps are sequences too, like 1️⃣ = 1 + U+FE0F + combining keycap mark.

This is a non-exhaustive list. There are many more characters that can be displayed as both text and emoji.

How to Detect Them

Log the code points:

;[...str].map((c) => c.codePointAt(0).toString(16))

You'll see something like:

;['2764', 'fe0f']

That fe0f is the hidden culprit!

How to Fix Your Broken Tests

You've got options:

  • If you compare user input, normalize first. Normalize your strings (if appropriate for your use case)
  • Ensure both sides use the same literal
  • Strip variation selectors if emoji presentation doesn't matter
  • Be explicit in tests about which form you expect. Pick one representation and stick to it project-wide.

For example, to remove variation selectors:

str.replace(/\uFE0F/g, '')

Does the opposite exist?

Yes, there is a variation selector for text presentation. It's U+FE0E.

U+FE0E is Variation Selector-15 (VS15). It forces a character to render in text presentation instead of emoji presentation (no colors, only text/typographic style).

U+2764 U+FE0E  →  ❤

Try it in the console:

console.log('\u2764\uFE0E') // "❤"

You'll see it renders as a simple black heart.

Conclusion

This is a tiny Unicode detail that can break your tests. But it also taught me a little bit about Unicode and how it works.

This is the key to understanding how Unicode handles both iconography and typography.

In summary, you will understand perfectly the different looking of emojis and text by looking at this table taken from the :

It's wild how one invisible code point decides which side you're on. I'm glad I learned something new today!

⚠️ Security Considerations ⚠

U+FE0F (Variation Selector-16) and U+FE0E (Variation Selector-15) are considered hidden characters and some security scanners could flag them as potential security vulnerabilities. This is because some characters have been used for malicious purposes, like the .

So, yes, be careful of what you copy and paste from the Internet before pasting it in your code, your console, or your AI chat. 🙃