Skip to main content

Emoji to Unicode: Reading the Code Points Behind Every Glyph

How an emoji maps to U+XXXX code points, why some sit above U+FFFF and need surrogate pairs, the \u and U+ formats, and how ZWJ joins several into one glyph.

Published By Li Lei
#emoji #unicode #code-points #encoding #javascript

Emoji to Unicode: Reading the Code Points Behind Every Glyph

An emoji looks like one small picture, but underneath it is a number, or a short list of numbers. Each of those numbers is a Unicode code point, and once you can read them, a lot of confusing emoji behavior stops being mysterious. The grinning face you copied from a chat is really U+1F600. A family is really seven separate things glued together. A flag is two letters in disguise.

This post walks through how an emoji maps to its code points, why those numbers are written as U+XXXX and \u{...}, what surrogate pairs are and when they show up, and why some emoji expand into a whole chain of code points joined by an invisible character. If you want to follow along with a real input, paste any emoji into the Emoji to Unicode converter and watch the sequence split apart as you read.

One emoji, one or more code points

The first thing to internalize: an emoji maps to one or more code points, never assume exactly one.

A simple emoji is a single code point. The grinning face 😀 is U+1F600 and nothing else. That is the whole story for it. You can verify this with one paste — the tool shows the single code point, plus the decimal 128512, the HTML entity 😀, the JavaScript escape \u{1F600}, and the CSS escape \1F600, all pointing at that one character.

But plenty of emoji are not single code points. A family 👨‍👩‍👧‍👦 is seven. A waving hand with a skin tone 👋🏽 is two. A flag 🇯🇵 is two. When you treat one of those as a single character — slicing a string by index, counting length, comparing the first unit only — you cut the emoji in half. The picture you see on screen is a grapheme: the unit a human reads as one symbol. The code points are what the encoding actually stores, and the two counts often disagree.

Why the numbers are written as U+XXXX and \u

A code point is just an integer, but you will see it written several ways depending on where it lives.

  • U+1F600 is the Unicode standard notation. The U+ prefix says "this is a code point" and the digits after it are hexadecimal. This is what you read in spec tables and bug reports.
  • \u{1F600} is a JavaScript string escape. The braces let you write any code point, including the big ones above U+FFFF. Without braces, \u in JavaScript only accepts exactly four hex digits, which is a trap I'll come back to.
  • \1F600 is the CSS escape, used in content properties.
  • 😀 (hex) and 😀 (decimal) are HTML numeric entities. Emoji have no named entities, so the numeric form is the only option.

All of those are the same number, 0x1F600, dressed for different contexts. The hex value matters more than the costume. If you want to go the other direction and look up which character a raw hex value belongs to, the Unicode character inspector takes a bare code point and tells you the name, block, and category.

Code points vs surrogate pairs

Here is the part that bites people writing code.

Unicode code points run from U+0000 up past U+10FFFF. Anything at or below U+FFFF fits in a single 16-bit unit. Most emoji do not. The grinning face U+1F600 is well above U+FFFF, and so are the great majority of pictographic emoji, which live in the range starting at U+1F300.

UTF-16 — the encoding JavaScript strings use internally — can only hold 16 bits per unit. To store a code point above U+FFFF, it splits the value into two 16-bit halves called a surrogate pair: a high surrogate in the range U+D800U+DBFF and a low surrogate in U+DC00U+DFFF. Together those two units encode one code point.

That split is why old emoji-counting code breaks. '😀'.length returns 2, not 1, because the string holds two UTF-16 units. '😀'.charCodeAt(0) returns 55357 — only the high surrogate, half of the pair. If you slice a string between those two halves, you produce a lone surrogate, which renders as a replacement box. The fix is to read code points, not units: '😀'.codePointAt(0) returns the real 128512, and [...'😀'].length or an Intl.Segmenter gives you a count that matches what you see. The converter uses exactly that grapheme-aware splitting, which is why it never reports half a character.

ZWJ sequences: several code points joined into one glyph

The most surprising emoji are the ones built by joining others. The mechanism is a single invisible character: the zero-width joiner, U+200D.

When the renderer sees two emoji with a U+200D between them, it tries to draw them as one combined glyph. Stack several together and you get a family, a profession, a couple, or a heart with a specific color. The glyph looks like one symbol, but the stored sequence is a chain.

A worked example

Take two cases side by side.

The grinning face is the easy case:

😀  →  U+1F600

One grapheme, one code point. Decimal 128512, HTML 😀, JS \u{1F600}.

Now the family 👨‍👩‍👧‍👦, which is a ZWJ sequence:

👨‍👩‍👧‍👦  →  U+1F468 U+200D U+1F469 U+200D U+1F467 U+200D U+1F466
              man     ZWJ     woman   ZWJ     girl    ZWJ     boy

Four people and three joiners — seven code points total — that a renderer fuses into one picture of a family. This is exactly where invisible-character bugs hide. If a sanitizer or a careless copy step strips the U+200D joiners, the sequence falls apart into four separate people standing in a row. When that happens to me, the first thing I do is paste the broken emoji into the converter and check the code point list: if the U+200D markers are missing, I know the joiners were eaten somewhere upstream, and I can point at the exact step instead of guessing. Being able to prove it code point by code point has saved me more than one argument about whose pipeline mangled the data.

Skin tones and flags work on the same principle of multiple code points, but without a joiner. A waving hand with a medium tone 👋🏽 is the base U+1F44B followed by the modifier U+1F3FD (one of five tone modifiers from U+1F3FB to U+1F3FF). A flag 🇯🇵 is two regional-indicator letters, U+1F1EF and U+1F1F5 — the letters J and P drawn as a flag.

Putting it to work

Once you can read code points, several everyday tasks get easier.

Embedding an emoji in source code that mangles raw bytes? Copy the \u{1F600} escape and your string survives any build step. Writing markup that needs a flag? You can't use a single entity, so you write each code point: 🇯🇵. If you assemble a lot of those, the HTML entities encoder handles whole strings at once, and the text to hex converter is handy when you need the raw byte view rather than code points. And when you have a code point but not the glyph — a \u{1F600} sitting in a log file — paste it into the Unicode → Emoji direction and read the emoji back. If you don't know the emoji's name to begin with, the emoji finder lets you search by keyword first, then convert.

The mental model is the part that lasts. An emoji is a grapheme you see; it is one or more code points the machine stores; above U+FFFF each code point becomes a surrogate pair in UTF-16; and a U+200D joiner can chain several code points into one glyph. Hold those four facts and emoji stop being a black box. The numbers were there all along.


Made by Toolora · Updated 2026-06-13