Emoji Version Detection and Graceful Degradation

Emoji Versioning and Support Detection

Unicode releases a new emoji set roughly every year. Emoji 15.0 introduced 31 new characters; Emoji 16.0 (released in late 2024) added more. Platforms (iOS, Android, Windows) update their emoji fonts with operating system releases, meaning users on older OS versions cannot see newer emoji.

When a platform does not support an emoji, it renders as a tofu box (☐) or as the text description in brackets. Understanding how to detect and handle this gracefully keeps your UI from looking broken.

How Emoji Versions Work

Emoji versions are distinct from Unicode versions, though they track together. Key milestones:

Emoji Version Unicode Version Year Notable additions
1.0 8.0 2015 722 baseline emoji
3.0 9.0 2016 🤣 🤞 🦊 🥑
5.0 10.0 2017 🤩 🧠 🦷 🥗
11.0 11.0 2018 🥰 🦸 🧩 🥳
12.0 12.0 2019 🧑‍🦯 🦦 🧆 🥻
13.0 13.0 2020 🥲 🫀 🦬 🪲
14.0 14.0 2021 🫣 🫶 🪸 🩻
15.0 15.0 2022 🫨 🪼 🩵 🫷
15.1 15.1 2023 🙂‍↕️ 🙂‍↔️ 🍋‍🟩 👁️‍🗨️
16.0 16.0 2024 🫩 🪉 🫎 🐦‍🔥

Each emoji in emoji-test.txtemoji-test.txt
모든 이모지 시퀀스의 자격 상태, 코드 포인트, CLDR 이름을 나열하는 유니코드 공식 파일.
carries a version annotation in a comment:

1F600 ; fully-qualified     # 😀 E1.0 grinning face
1FAE8 ; fully-qualified     # 🫨 E15.0 shaking face

Platform Support Lag

After Unicode releases an emoji version, platforms deploy support through OS updates:

Emoji Version iOS support Android support Windows support
15.0 iOS 16.4 Android 13 Windows 11 22H2
15.1 iOS 17.4 Android 14 Windows 11 23H2
16.0 iOS 18.2 Android 15 Windows 11 24H2

This means users on iOS 16 cannot display Emoji 15.0 characters — they see a tofu box instead.

Detecting Emoji Support in the Browser

Canvas Pixel Comparison Method

The most reliable client-side detection renders the emoji on a canvas and checks whether the pixels differ from a known unsupported character:

const emojiSupportCache = new Map();

function supportsEmoji(emoji) {
  if (emojiSupportCache.has(emoji)) {
    return emojiSupportCache.get(emoji);
  }

  const canvas = document.createElement('canvas');
  canvas.width = 2;
  canvas.height = 2;
  const ctx = canvas.getContext('2d', { willReadFrequently: true });
  ctx.font = '14px Arial';

  // Baseline: a character definitely not supported (use a PUA code point)
  ctx.fillText('\uDB40\uDC00', 0, 14); // Tag character fallback
  const baseline = ctx.getImageData(0, 0, 1, 1).data.slice(0, 3);

  ctx.clearRect(0, 0, 2, 2);
  ctx.fillText(emoji, 0, 14);
  const tested = ctx.getImageData(0, 0, 1, 1).data.slice(0, 3);

  // If color data differs, the emoji rendered as a glyph
  const supported = !baseline.every((v, i) => v === tested[i]);
  emojiSupportCache.set(emoji, supported);
  return supported;
}

// Check specific emoji versions
const checks = {
  emoji12: "🥲",   // E12.0 — 2020
  emoji13: "🫀",   // E13.0 — 2021
  emoji14: "🫣",   // E14.0 — 2022
  emoji15: "🫨",   // E15.0 — 2022
  emoji16: "🫩",   // E16.0 — 2024
};

for (const [version, char] of Object.entries(checks)) {
  console.log(`${version} (${char}): ${supportsEmoji(char) ? '✅ supported' : '❌ not supported'}`);
}

Detecting ZWJ영폭 결합자 (ZWJ)
여러 이모지를 하나의 합성 이모지로 결합하는 데 사용되는 보이지 않는 유니코드 문자(U+200D).
Sequence Support

ZWJ sequences can partially render even when the combined glyph is not supported — the individual components show instead. To detect whether a ZWJ sequence renders as one glyph vs변형 선택자 (VS)
문자를 텍스트(단색) 또는 이모지(컬러) 표현으로 렌더링할지 결정하는 유니코드 문자.
. multiple:

function supportsZWJSequence(zwjEmoji) {
  const canvas = document.createElement('canvas');
  canvas.width = 80;
  canvas.height = 20;
  const ctx = canvas.getContext('2d', { willReadFrequently: true });
  ctx.font = '14px Arial';

  // Measure the ZWJ sequence
  const zwjWidth = ctx.measureText(zwjEmoji).width;

  // Measure the components separately (without ZWJ)
  const parts = [...zwjEmoji].filter(c => c !== '\u200D');
  const partsWidth = ctx.measureText(parts.join('')).width;

  // If widths are similar, ZWJ sequence is not supported (renders as parts)
  // If ZWJ is narrower, it's a single glyph
  return zwjWidth < partsWidth * 0.9;
}

console.log(supportsZWJSequence("👨‍💻")); // true on modern platforms
console.log(supportsZWJSequence("🧑‍🦲")); // may vary by platform/OS version

Detecting Emoji Support in Python

Server-side detection is less reliable since you don't have access to the client's font stack. The best approach is to use the client's User-Agent to infer emoji support:

from dataclasses import dataclass
import re

@dataclass
class EmojiVersionInfo:
    min_emoji_version: float
    os_name: str

def detect_emoji_version(user_agent: str) -> EmojiVersionInfo:
    """Approximate emoji version support from User-Agent string."""

    # iOS version → emoji version mapping
    if "iPhone OS" in user_agent or "iPad; CPU OS" in user_agent:
        match = re.search(r'OS (\d+)_', user_agent)
        if match:
            ios_ver = int(match.group(1))
            version_map = {18: 16.0, 17: 15.1, 16: 15.0, 15: 14.0, 14: 13.0}
            for ios, emoji in version_map.items():
                if ios_ver >= ios:
                    return EmojiVersionInfo(emoji, "iOS")
        return EmojiVersionInfo(11.0, "iOS")

    # Android version → emoji version (via Noto updates)
    if "Android" in user_agent:
        match = re.search(r'Android (\d+)', user_agent)
        if match:
            android_ver = int(match.group(1))
            version_map = {15: 16.0, 14: 15.1, 13: 15.0, 12: 13.1, 11: 13.0}
            for android, emoji in version_map.items():
                if android_ver >= android:
                    return EmojiVersionInfo(emoji, "Android")
        return EmojiVersionInfo(11.0, "Android")

    # Windows 11 generally has recent emoji support
    if "Windows NT 10.0" in user_agent:
        return EmojiVersionInfo(15.0, "Windows")

    return EmojiVersionInfo(11.0, "Unknown")

Graceful Degradation Strategies

1. Image Fallback for Specific New Emoji

If you use a specific new emoji that may not be supported, provide an image fallback:

<!-- Fallback: emoji → image for unsupported platforms -->
<picture>
  <source media="(min-resolution: 1dpi)" srcset="/emoji/shaking-face.png">
  <!-- Modern platforms show this text, which triggers the image source on older -->
  <img src="/emoji/shaking-face.png" alt="Shaking face" width="20" height="20">
</picture>

Or use JavaScript to replace unsupported emoji dynamically:

const NEW_EMOJI = ["🫨", "🪼", "🩵", "🫷", "🫸"]; // E15.0+

document.addEventListener('DOMContentLoaded', () => {
  NEW_EMOJI.forEach(emoji => {
    if (!supportsEmoji(emoji)) {
      // Replace with Twemoji image
      const imgUrl = getTwemojiUrl(emoji);
      document.querySelectorAll('*').forEach(el => {
        if (el.childNodes.length === 1 && el.childNodes[0].nodeType === 3) {
          el.innerHTML = el.innerHTML.replace(
            emoji,
            `<img src="${imgUrl}" alt="${emoji}" style="height:1em;vertical-align:-0.1em;">`
          );
        }
      });
    }
  });
});

2. Twemoji for Universal Rendering

Twemoji (from Twitter/X) renders all emoji as SVG/PNG images from a CDN, ensuring consistent display across all platforms regardless of OS emoji version:

import twemoji from 'twemoji';

// Replace emoji with images sitewide
twemoji.parse(document.body, {
  folder: 'svg',
  ext: '.svg',
  base: 'https://cdn.jsdelivr.net/gh/twitter/twemoji@latest/assets/',
  className: 'twemoji',
});
.twemoji {
  height: 1em;
  width: 1em;
  margin: 0 0.05em 0 0.1em;
  vertical-align: -0.1em;
}

3. Emoji Version Tag in Your Data

When storing user-generated emoji content, record the minimum emoji version required:

import regex

# Map emoji to their version (from emoji-data.txt parsing)
EMOJI_VERSION_MAP: dict[str, float] = {
    "🫨": 15.0,
    "🪼": 15.0,
    "🥲": 13.0,
    "🤣": 3.0,
    "😀": 1.0,
    # ... full dataset
}

def required_emoji_version(text: str) -> float:
    """Return the minimum emoji version needed to display this text."""
    found = regex.findall(r'\X', text)
    max_version = 0.0
    for grapheme in found:
        # Use the first code point of the cluster for lookup
        version = EMOJI_VERSION_MAP.get(grapheme, 0.0)
        max_version = max(max_version, version)
    return max_version

Explore More on EmojiFYI

관련 도구

🔀 플랫폼 비교 플랫폼 비교
Apple, Google, Samsung, Microsoft 등 다양한 플랫폼에서 이모지가 어떻게 표시되는지 비교해 보세요. 시각적 차이를 나란히 확인할 수 있습니다.
🔍 시퀀스 분석기 시퀀스 분석기
ZWJ 시퀀스, 피부톤 수정자, 키캡 시퀀스, 국기 쌍을 개별 구성 요소로 분해합니다.

용어 사전 항목

이모지 이모지
일본어 '그림 문자(絵文字)'를 뜻하며, 디지털 커뮤니케이션에서 감정과 개념을 표현하는 작은 그래픽 기호.
유니코드 유니코드
모든 문자 체계와 이모지를 포함하여 각 문자에 고유한 번호를 부여하는 범용 문자 인코딩 표준.
트웨모지 트웨모지
트위터가 만든 오픈소스 이모지 세트로, SVG와 PNG 에셋을 자유롭게 사용할 수 있습니다.
이모지 버전 이모지 버전
이모지가 처음 도입된 릴리스 버전으로, 2016년 이후 매년 릴리스됩니다.
영폭 결합자 (ZWJ) 영폭 결합자 (ZWJ)
여러 이모지를 하나의 합성 이모지로 결합하는 데 사용되는 보이지 않는 유니코드 문자(U+200D).
코드 포인트 코드 포인트
유니코드 표준에서 각 문자에 부여되는 고유한 숫자 값으로, U+XXXX 형식으로 표기됩니다.
emoji-test.txt emoji-test.txt
모든 이모지 시퀀스의 자격 상태, 코드 포인트, CLDR 이름을 나열하는 유니코드 공식 파일.

관련 스토리