📖 How-To Guides

How to Handle Emojis in JavaScript: Strings, Length, and Rendering

Emojis in JavaScript: Why It's Tricky

JavaScript makes it easy to display emojis — just put them in a string. But processing emoji strings correctly requires understanding some important quirks in how JavaScript handles Unicode internally. If you've ever seen an emoji counted as 2 characters, or a string manipulation function split an emoji in half, this guide explains why and how to fix it.

Basic Emoji Strings in JavaScript

You can include emoji characters directly in JavaScript strings:

const greeting = "Hello 🌍"
const status = `Build complete ✅ — version ${version}`
console.log("🚀 Server starting...")

JavaScript strings are UTF-16UTF-16
每字符使用2或4个字节的可变长度Unicode编码方式,被JavaScript、Java和Windows内部使用。
encoded internally. Most emojis are in the Unicode Supplementary Multilingual Plane (code points above U+FFFF) and require two UTF-16 code units — called a surrogate pair — to represent a single visible emoji.

Unicode Code Points in JavaScript

ES6 introduced a clean syntax for any Unicode code point:

const fire = "\u{1F525}"     // 🔥
const grin = "\u{1F600}"     // 😀
const heart = "\u{2764}\u{FE0F}"  // ❤️ (two code points)

console.log(fire)  // 🔥

Legacy Surrogate Pair Syntax (Pre-ES6)

Before ES6, you had to write the surrogate pair manually for high code points:

// 🔥 U+1F525 as surrogate pair
const fire = "\uD83D\uDD25"
console.log(fire)  // 🔥

The ES6 \u{} syntax is far more readable and should be preferred in all modern code.

Getting an Emoji's Code Point

// codePointAt handles surrogate pairs correctly
const emoji = "🔥"
console.log(emoji.codePointAt(0))           // 128293 (decimal)
console.log(emoji.codePointAt(0).toString(16))  // "1f525" (hex)

// String.fromCodePoint creates from a code point
console.log(String.fromCodePoint(0x1F525))  // 🔥
console.log(String.fromCodePoint(128293))   // 🔥

The String Length Problem

The most common emoji pitfall in JavaScript is that .length counts UTF-16 code units, not visible characters.

// Basic emoji: 2 code units (surrogate pair)
console.log("🔥".length)     // 2 — but it's 1 emoji

// Skin tone emoji: 4 code units (base + modifier)
console.log("👍🏽".length)   // 4 — but it's 1 emoji

// ZWJ零宽连接符(ZWJ) (ZWJ)
一个不可见的Unicode字符(U+200D),用于将多个表情符号合并为单一复合表情符号,例如将人物与物体组合成职业表情符号。
sequence (woman technologist): 7 code units console.log("👩‍💻".length) // 7 — but it's 1 emoji // A simple ASCII character for comparison console.log("A".length) // 1

Counting by Code Point (Better, But Still Not Perfect)

The spread operator and Array.from iterate by code point rather than code unit:

console.log([..."🔥"].length)         // 1 ✓
console.log([..."👍🏽"].length)       // 2 — base + skin modifier
console.log([..."👩‍💻"].length)      // 3 — components of ZWJ sequence
console.log([..."👨‍👩‍👧‍👦"].length)  // 7 — family ZWJ sequence

This is better than .length but still doesn't match visual character count for complex sequences.

Counting Grapheme Clusters (Correct)

The correct solution uses the Intl.Segmenter API (available in Node.js 16+ and all modern browsers):

function graphemeCount(str) {
  const segmenter = new Intl.Segmenter()
  return [...segmenter.segment(str)].length
}

console.log(graphemeCount("Hello"))        // 5
console.log(graphemeCount("Hello 🔥"))     // 7
console.log(graphemeCount("👩‍💻"))        // 1 ✓
console.log(graphemeCount("👨‍👩‍👧‍👦"))    // 1 ✓
console.log(graphemeCount("👍🏽"))         // 1 ✓

For older environments, the grapheme-splitter npm package provides equivalent functionality:

npm install grapheme-splitter
import GraphemeSplitter from "grapheme-splitter"

const splitter = new GraphemeSplitter()
console.log(splitter.countGraphemes("👩‍💻"))  // 1

Iterating Over Emoji Strings

Avoid for loops with index when iterating emoji strings — you'll split surrogate pairs:

// WRONG: splits surrogate pairs
const text = "Hi 🔥"
for (let i = 0; i < text.length; i++) {
  console.log(text[i])  // splits 🔥 into two broken characters
}

// CORRECT: iterate by code point with for...of
for (const char of "Hi 🔥") {
  console.log(char)  // H, i, " ", 🔥
}

// CORRECT: use spread
const chars = [..."Hi 🔥"]
console.log(chars)  // ["H", "i", " ", "🔥"]

For ZWJ sequences and skin tone modifiers, even for...of splits the components. Use Intl.Segmenter for truly correct grapheme-level iteration:

const text = "👩‍💻👍🏽🔥"
const segmenter = new Intl.Segmenter()
const segments = [...segmenter.segment(text)].map(s => s.segment)
console.log(segments)  // ["👩‍💻", "👍🏽", "🔥"]

Regex and Emojis in JavaScript

The /u flag enables Unicode mode in JavaScript regex, allowing you to match full Unicode code points rather than UTF-16 code units:

// Without /u: matches one surrogate unit (broken)
/\uD83D/.test("🔥")  // true (wrong — matches half the surrogate pair)

// With /u: matches the full code point
/\u{1F525}/u.test("🔥")  // true (correct)

// Match any emoji in supplementary plane
const emojiRegex = /\p{Emoji}/u
console.log(emojiRegex.test("🔥"))  // true
console.log(emojiRegex.test("A"))   // false

Using the Unicode Property Escape \p{Emoji}

Modern JavaScript (ES2018+) supports Unicode property escapes:

// Match sequences of emoji characters
const emojiPattern = /\p{Emoji_Presentation}/gu

const text = "Hello 🌍! Great work ✅ today 🚀"
const emojis = text.match(emojiPattern)
console.log(emojis)  // ["🌍", "✅", "🚀"]

// Remove all emojis from a string
const cleaned = text.replace(/\p{Emoji_Presentation}/gu, "").trim()
console.log(cleaned)  // "Hello ! Great work  today"

For comprehensive emoji matching including ZWJ sequences, use the emoji-regex npm package:

npm install emoji-regex
import emojiRegex from "emoji-regex"

const regex = emojiRegex()
const text = "Hello 👩‍💻 and 👍🏽!"
const matches = [...text.matchAll(regex)]
console.log(matches.map(m => m[0]))  // ["👩‍💻", "👍🏽"]

Emojis in the DOM

In the browser, you can set emoji text content through standard DOM APIs:

// textContent is safe — emojis are just text
document.getElementById("status").textContent = "Build complete ✅"

// innerHTML works too, and you can use HTML entities
element.innerHTML = "Hello &#x1F525;"  // 🔥

// Creating elements with emojis
const btn = document.createElement("button")
btn.textContent = "🚀 Launch"
btn.setAttribute("aria-label", "Launch rocket")
document.body.appendChild(btn)

Emojis in React

React handles emoji strings without issues — they're just text:

// Inline emoji
function StatusBadge({ status }) {
  const icons = {
    success: "✅",
    error: "❌",
    pending: "⏳",
  }
  return <span>{icons[status]} {status}</span>
}

// Emoji in JSX with aria-label for accessibility
function EmojiIcon({ emoji, label }) {
  return (
    <span role="img" aria-label={label}>
      {emoji}
    </span>
  )
}

// Usage
<EmojiIcon emoji="🚀" label="rocket" />

Practical Utility Functions

Check if a String Contains Emojis

function containsEmoji(str) {
  return /\p{Emoji_Presentation}/u.test(str)
}

console.log(containsEmoji("Hello 🌍"))   // true
console.log(containsEmoji("Hello"))       // false

Extract All Emojis from a String

import emojiRegex from "emoji-regex"

function extractEmojis(str) {
  const regex = emojiRegex()
  return [...str.matchAll(regex)].map(m => m[0])
}

console.log(extractEmojis("I 🔥 love ❤️ JavaScript 🚀"))
// ["🔥", "❤️", "🚀"]

Truncate Text Preserving Emoji Integrity

function truncate(str, maxGraphemes) {
  const segmenter = new Intl.Segmenter()
  const segments = [...segmenter.segment(str)]
  if (segments.length <= maxGraphemes) return str
  return segments.slice(0, maxGraphemes).map(s => s.segment).join("") + "…"
}

console.log(truncate("Hello 👩‍💻 World! 🔥", 8))
// "Hello 👩‍💻 W…" — emoji counted as 1 character

Explore More on EmojiFYI

  • Inspect the Unicode code points behind any emoji with the Sequence Analyzer
  • Browse and copy emojis for your JavaScript strings with the Emoji Keyboard
  • Learn about grapheme clusters, ZWJ sequences, and surrogate pairs in the Glossary
  • Search for any emoji by name or keyword at EmojiFYI Search

相关工具

⌨️ 表情符号键盘 表情符号键盘
按分类浏览并复制 3,953 个表情符号。适用于任何浏览器,无需安装。
🔍 序列分析器 序列分析器
将 ZWJ 序列、肤色修饰符、键帽序列和旗帜对解析为独立组成部分。

术语表词条

UTF-16 UTF-16
每字符使用2或4个字节的可变长度Unicode编码方式,被JavaScript、Java和Windows内部使用。
代理对 代理对
用于表示U+FFFF以上字符的两个UTF-16编码单元组合,由高代理(U+D800-U+DBFF)和低代理(U+DC00-U+DFFF)构成。
平面 平面
由65,536个连续Unicode码位构成的分组。第0平面为基本多文种平面(BMP),大多数表情符号位于第1平面(SMP)。
码位 码位
Unicode标准中分配给每个字符的唯一数值,以U+XXXX格式表示(例如😀对应U+1F600)。
统一码 统一码
为所有书写系统和符号集(包括表情符号)中的每个字符分配唯一编号的通用字符编码标准。
编码单元 编码单元
用于编码字符的最小位组合:UTF-8为8位,UTF-16为16位,UTF-32为32位。
补充多文种平面(SMP) (SMP) 补充多文种平面(SMP) (SMP)
Unicode第1平面(U+10000至U+1FFFF),绝大多数表情符号码位都分配在此区域。
表情符号(Emoji) 表情符号(Emoji)
来自日语「絵文字」(えもじ),意为「图画文字」,是用于数字通信中表达想法、情感和事物的小型图形符号。
零宽连接符(ZWJ) (ZWJ) 零宽连接符(ZWJ) (ZWJ)
一个不可见的Unicode字符(U+200D),用于将多个表情符号合并为单一复合表情符号,例如将人物与物体组合成职业表情符号。

相关故事