Text vs Emoji Presentation Selectors: VS15 (U+FE0E) and VS16 (U+FE0F)

Text vs变体选择符 (VS)
用于控制字符以文本形式(单色)还是表情符号形式(彩色)显示的Unicode字符。VS-15(U+FE0E)指定文本样式,VS-16(U+FE0F)指定表情符号样式。
Emoji Presentation Selectors

Many Unicode characters have two visual forms: a monochrome text symbol and a full-color emoji. The character ☎ (U+260E BLACK TELEPHONE) can appear as ☎︎ (text, black outline) or ☎️ (emoji, with color). The choice is controlled by variation selectors — invisible Unicode code points that follow the base character.

Understanding variation selectors is essential for consistent rendering, correct text processing, and accurate emoji detection.

The Two Emoji Variation Selectors

Selector Code Point Name Effect
VS15 U+FE0E VARIATION SELECTOR-15 Forces text presentation
VS16 U+FE0F VARIATION SELECTOR-16 Forces emoji presentation

These selectors are combining characters — they apply to the immediately preceding base character and are not displayed themselves.

Examples

# Python
text_phone    = "\u260E\uFE0E"   # ☎ + VS15 → ☎︎  (text)
emoji_phone   = "\u260E\uFE0F"   # ☎ + VS16 → ☎️  (emoji)
default_phone = "\u260E"         # ☎        → ☎   (platform default: text)

# These look different in supporting renderers:
print(text_phone)    # ☎︎  monochrome
print(emoji_phone)   # ☎️  colored
print(default_phone) # ☎   (depends on platform)
const textPhone  = "\u260E\uFE0E"; // ☎︎
const emojiPhone = "\u260E\uFE0F"; // ☎️

// Note: these are different strings
console.log(textPhone === emojiPhone); // false
console.log(textPhone.length);         // 2
console.log(emojiPhone.length);        // 2

Which Characters Have Dual Presentations?

The Unicode Standard defines an Emoji_Presentation property and a separate table of characters that support both text and emoji presentation — characters that are Emoji=Yes but Emoji_Presentation=No. These are text-default emoji: they render as text symbols unless followed by VS16.

Common text-default emoji include:

Character Code Point Default With VS16
☎ U+260E ☎ text ☎️ emoji
⌚ U+231A ⌚ text ⌚️ emoji
✂ U+2702 ✂ text ✂️ emoji
❤ U+2764 ❤ text ❤️ emoji
✈ U+2708 ✈ text ✈️ emoji
☀ U+2600 ☀ text ☀️ emoji
© U+00A9 © text ©️ emoji
® U+00AE ® text ®️ emoji
‼ U+203C ‼ text ‼️ emoji

The full list is in emoji-variation-sequences.txt in the Unicode UCD.

Fully-Qualified vs Minimally-Qualified Emoji

The Unicode standard defines fully-qualified emoji as those with all required variation selectors present. The emoji-test.txtemoji-test.txt
Unicode官方文件,列出所有表情符号序列及其限定状态、码位和CLDR短名称。
file marks each emoji sequence as:

  • fully-qualified: All VS16 selectors present (e.g., ❤️ = U+2764 + U+FE0F)
  • minimally-qualified: VS16 absent from one or more expected positions
  • unqualified: No variation selectors
# From emoji-test.txt:
2764 FE0F    ; fully-qualified     # ❤️ E0.6 red heart
2764          ; unqualified         # ❤ E0.6 red heart

For reliable emoji lookup and comparison, normalize all emoji to their fully-qualified form.

Detecting and Normalizing Variation Selectors

Strip All Variation Selectors (Python)

import regex

VS15 = "\uFE0E"
VS16 = "\uFE0F"

def strip_variation_selectors(text: str) -> str:
    """Remove VS15 and VS16 from text."""
    return text.replace(VS15, "").replace(VS16, "")

def normalize_to_emoji(text: str) -> str:
    """
    Convert text-default emoji to emoji presentation
    where a VS16 form exists.
    """
    # Strip any existing selectors first
    clean = strip_variation_selectors(text)
    # Add VS16 after each character that has an emoji presentation
    result = []
    for char in clean:
        result.append(char)
        # Check if this character has an emoji VS16 form
        if has_emoji_presentation(char):
            result.append(VS16)
    return "".join(result)

def has_emoji_presentation(char: str) -> bool:
    """Return True if char has a VS16 emoji presentation form."""
    TEXT_DEFAULT_EMOJI = {
        "\u00A9", "\u00AE", "\u203C", "\u2049", "\u2122",
        "\u2139", "\u2194", "\u2195", "\u2196", "\u2197",
        "\u2198", "\u2199", "\u21A9", "\u21AA", "\u231A",
        "\u231B", "\u2328", "\u23CF", "\u260E", "\u2611",
        "\u2614", "\u2615", "\u2618", "\u261D", "\u2620",
        "\u2622", "\u2623", "\u2626", "\u262A", "\u262E",
        "\u262F", "\u2638", "\u2639", "\u263A", "\u2640",
        "\u2642", "\u2648", "\u2702", "\u2708", "\u2764",
        # ... full list from emoji-variation-sequences.txt
    }
    return char in TEXT_DEFAULT_EMOJI

# Examples
print(strip_variation_selectors("❤️"))   # ❤ (just U+2764)
print(strip_variation_selectors("☎︎"))   # ☎ (just U+260E)
print(normalize_to_emoji("❤"))           # ❤️ (adds VS16)

JavaScript Implementation

const VS15 = '\uFE0E';
const VS16 = '\uFE0F';

function stripVariationSelectors(str) {
  return str.replace(/[\uFE0E\uFE0F]/g, '');
}

function getVariationSelectorType(char) {
  if (char === VS15) return 'text';
  if (char === VS16) return 'emoji';
  return null;
}

// Check if a string position has a variation selector following it
function getPresentation(text, index) {
  const nextChar = text[index + 1];
  if (nextChar === VS16) return 'emoji';
  if (nextChar === VS15) return 'text';

  // No selector — check Emoji_Presentation property
  const char = String.fromCodePoint(text.codePointAt(index));
  return /^\p{Emoji_Presentation}$/u.test(char) ? 'emoji' : 'text';
}

// Normalize: ensure emoji presentation for emoji-capable characters
const TEXT_DEFAULT_EMOJI = new Set([
  '\u00A9', '\u00AE', '\u203C', '\u2122', '\u2139',
  '\u260E', '\u2702', '\u2708', '\u2764',
  // ... full set
]);

function ensureEmojiPresentation(text) {
  let result = '';
  for (const char of text) {
    result += char;
    if (char !== VS15 && char !== VS16 && TEXT_DEFAULT_EMOJI.has(char)) {
      result += VS16;
    }
  }
  return result;
}

Impact on String Comparison

Variation selectors make otherwise-identical-looking strings unequal:

heart_no_vs  = "❤"   # U+2764
heart_vs16   = "❤️"  # U+2764 + U+FE0F

print(heart_no_vs == heart_vs16)  # False
print(len(heart_no_vs))           # 1
print(len(heart_vs16))            # 2

# For comparison purposes, normalize first:
def normalize_emoji_for_compare(text: str) -> str:
    return strip_variation_selectors(text)

print(normalize_emoji_for_compare("❤️") == normalize_emoji_for_compare("❤"))  # True

This matters for: - Database lookups: Searching for ❤ won't match ❤️ stored with VS16 - Emoji counting: ❤ and ❤️ should count as the same emoji - Deduplication: Two users reacting with ❤ and ❤️ should be merged

Variation Selectors in Keycap Sequences

Digit emoji (1️⃣ through 9️⃣, 0️⃣, #️⃣, *️⃣) are three-code-point sequences:

1️⃣ = U+0031 (digit "1") + U+FE0F (VS16) + U+20E3 (combining enclosing keycap)

The VS16 here is mandatory — without it, the keycap sequence is malformed in many renderers:

keycap_correct   = "1\uFE0F\u20E3"  # 1️⃣ — correct
keycap_malformed = "1\u20E3"        # 1⃣  — may not render correctly

# Fully-qualified check
import emoji
print(emoji.is_emoji(keycap_correct))   # True
print(emoji.is_emoji(keycap_malformed)) # Varies by library version

CSS and HTML Implications

In HTML, browsers generally handle variation selectors automatically. However, when you construct text dynamically via JavaScript or server-side templates, you must include VS16 explicitly for text-default emoji:

<!-- These may render differently depending on browser/OS -->
<span>❤</span>   <!-- text form — black outline on some platforms -->
<span>❤️</span>  <!-- emoji form — red heart everywhere -->

<!-- In JavaScript templates -->
const heartEmoji = "\u2764\uFE0F";  // Always emoji presentation
element.textContent = `I ${heartEmoji} JavaScript`;

Explore More on EmojiFYI

相关工具

🔀 平台比较 平台比较
比较表情符号在 Apple、Google、Samsung、Microsoft 等平台上的渲染效果,并排查看视觉差异。
🔍 序列分析器 序列分析器
将 ZWJ 序列、肤色修饰符、键帽序列和旗帜对解析为独立组成部分。

术语表词条

emoji-test.txt emoji-test.txt
Unicode官方文件,列出所有表情符号序列及其限定状态、码位和CLDR短名称。
Unicode标准 Unicode标准
由Unicode联盟维护的完整字符编码体系,定义字符、属性、算法和编码形式。
变体选择符 (VS) 变体选择符 (VS)
用于控制字符以文本形式(单色)还是表情符号形式(彩色)显示的Unicode字符。VS-15(U+FE0E)指定文本样式,VS-16(U+FE0F)指定表情符号样式。
彩色表情符号 彩色表情符号
使用位图图像或彩色矢量图形渲染的全彩表情符号,与单色文本样式渲染相对。
按键帽序列 按键帽序列
由数字或符号后接VS-16(U+FE0F)和组合包围键帽字符(U+20E3)构成的表情符号序列。
未限定 未限定
缺少必要变体选择符、可能无法在所有平台上正常渲染为表情符号的序列。
码位 码位
Unicode标准中分配给每个字符的唯一数值,以U+XXXX格式表示(例如😀对应U+1F600)。
统一码 统一码
为所有书写系统和符号集(包括表情符号)中的每个字符分配唯一编号的通用字符编码标准。
表情符号(Emoji) 表情符号(Emoji)
来自日语「絵文字」(えもじ),意为「图画文字」,是用于数字通信中表达想法、情感和事物的小型图形符号。
表情符号呈现 表情符号呈现
将字符渲染为彩色表情符号字形的默认显示方式,可通过字符本身属性或变体选择符-16触发。
表情符号序列 表情符号序列
由一个或多个Unicode码位按顺序组合,共同表示单个表情符号的字符集合。

相关故事