How Emoji Work: Skin Tones, ZWJ Sequences, and Modifiers

· 2 min read

Emoji look like simple pictures, but under the hood many of them are complex sequences of multiple Unicode code points. Understanding how emoji are constructed helps you manipulate them correctly in code, avoid common string-handling bugs, and appreciate the elegance of the Unicode design.

Base Emoji and Variation Selectors

Some characters in Unicode have both a text (monochrome) presentation and an emoji (colourful) presentation. The choice is controlled by variation selectors. U+FE0E (VS15) requests text presentation; U+FE0F (VS16) requests emoji presentation. For example, U+2764 ❤ followed by VS16 gives ❤️—the classic red heart emoji. Browse all emoji by category in the emoji browser.

Fitzpatrick Skin Tone Modifiers

The five skin tone modifiers (U+1F3FB through U+1F3FF) follow a base emoji to change its apparent skin tone. The hand-waving emoji 👋 (U+1F44B) plus U+1F3FD produces 👋🏽—a medium skin tone hand wave. These two code points render as a single glyph in supporting implementations. In JavaScript, "👋🏽".length === 4 because each is a surrogate pair occupying two UTF-16 code units.

Zero-Width Joiner (ZWJ) Sequences

The most complex emoji are built by joining multiple base emoji with U+200D Zero-Width Joiner. The family emoji 👨‍👩‍👧 is a sequence of three emoji separated by two ZWJ characters—six code points total, rendered as one glyph on platforms that support it. On unsupporting platforms each emoji is shown separately.

Common ZWJ sequences include profession emoji (👩‍💻 Woman Technologist = woman + ZWJ + laptop), rainbow flag (🏳️‍🌈 = white flag + VS16 + ZWJ + rainbow), and trans flag (🏳️‍⚧️). The full list of supported sequences is defined by the Unicode Emoji ZWJ Sequences chart.

Counting Emoji Correctly in Code

Because a single visible emoji may be many code points, naively counting characters with most language builtins gives wrong results. In JavaScript, use Array.from(str) or the Intl.Segmenter API (available in modern browsers and Node.js 16+) to split strings by grapheme cluster rather than code unit. In Python 3, iterating a string gives code points—close but still wrong for ZWJ sequences. For production-grade emoji handling, use a Unicode segmentation library.