Implementing Skin Tone Modifiers Programmatically

Implementing Skin Tone Modifiers Programmatically

Skin tone modifiers let users personalize emojiEmoji
Palabra japonesa (絵文字) que significa 'carácter imagen' — pequeños símbolos gráficos usados en la comunicación digital para expresar ideas, emociones y objetos.
to match their appearance. What looks like a single character — 👋🏽 — is actually two UnicodeUnicode
Estándar universal de codificación de caracteres que asigna un número único a cada carácter de todos los sistemas de escritura y conjuntos de símbolos, incluidos los emoji.
code points: a modifier base (👋 U+1F44B) followed by a skin tone modifier (🏽 U+1F3FD). Implementing this correctly requires understanding which emoji accept modifiers, how to apply and strip them, and how to handle complex multi-person ZWJConector de ancho cero (ZWJ)
Carácter Unicode invisible (U+200D) utilizado para unir varios emoji en un único emoji compuesto, como la combinación de personas y objetos en emoji de profesiones.
sequences.

The Fitzpatrick Scale

Unicode defines five skin tone modifier characters based on the Fitzpatrick dermatological scale:

Modifier Code Point Emoji Fitzpatrick Type
Light U+1F3FB 🏻 I–II (very light to light)
Medium-Light U+1F3FC 🏼 III (light brown)
Medium U+1F3FD 🏽 IV (moderate brown)
Medium-Dark U+1F3FE 🏾 V (dark brown)
Dark U+1F3FF 🏿 VI (deeply pigmented)

When a modifier follows a valid modifier base, both are rendered as a single skin-toned glyph. The modifier has no visual representation on its own.

Modifier Base Detection

Not all person or hand emoji accept skin tone modifiers. The Unicode property Emoji_Modifier_Base determines which do. Key examples:

Accept modifiers: 👋 👍 👎 ✌️ 🤞 👌 🤌 👏 🙌 🤝 🙏 💪 🦾 🖕 🖖 👶 👦 👧 🧑 👱 👴 👵 🧓 👲 👳 🧔 💁 🙋 🤷 🙎 🙍

Do NOT accept modifiers: 👻 🤖 🦊 🙊 👀 🔥 ❤️ (non-human or non-body-part emoji)

Checking Modifier Base in Python

import regex

def is_modifier_base(char: str) -> bool:
    """Return True if the character can accept a skin tone modifier."""
    return bool(regex.match(r'\p{Emoji_Modifier_Base}', char))

def is_skin_tone_modifier(char: str) -> bool:
    """Return True if the character is a skin tone modifier."""
    return bool(regex.match(r'\p{Emoji_Modifier}', char))

# Examples
print(is_modifier_base("👋"))   # True
print(is_modifier_base("🔥"))   # False
print(is_skin_tone_modifier("🏽")) # True
print(is_skin_tone_modifier("😊")) # False

Checking in JavaScript

// ES2018+ Unicode property escapes
const isModifierBase = (char) => /^\p{Emoji_Modifier_Base}$/u.test(char);
const isModifier = (char) => /^\p{Emoji_Modifier}$/u.test(char);

console.log(isModifierBase("👋")); // true
console.log(isModifierBase("🔥")); // false
console.log(isModifier("🏽"));    // true

Note: \p{Emoji_Modifier_Base} is not universally supported across all JavaScript engines. Test in your target environment or use a data-driven approach with a hardcoded set of modifier base code points.

Applying and Removing Modifiers

Apply a Modifier

SKIN_TONES = {
    "light":        "\U0001F3FB",  # 🏻
    "medium-light": "\U0001F3FC",  # 🏼
    "medium":       "\U0001F3FD",  # 🏽
    "medium-dark":  "\U0001F3FE",  # 🏾
    "dark":         "\U0001F3FF",  # 🏿
}

def apply_skin_tone(emoji: str, tone: str) -> str:
    """
    Apply a skin tone modifier to a modifier-base emoji.
    Removes any existing modifier first.
    Returns the original emoji unchanged if it doesn't accept modifiers.
    """
    if not is_modifier_base(emoji):
        return emoji

    # Strip existing modifier if present
    base = strip_skin_tone(emoji)

    modifier = SKIN_TONES.get(tone)
    if modifier is None:
        return base

    return base + modifier

def strip_skin_tone(emoji: str) -> str:
    """Remove skin tone modifier from an emoji, returning the default form."""
    modifier_range = regex.compile(r'[\U0001F3FB-\U0001F3FF]')
    return modifier_range.sub('', emoji)

# Usage
print(apply_skin_tone("👋", "medium"))        # 👋🏽
print(apply_skin_tone("👋🏻", "dark"))        # 👋🏿 (replaces existing)
print(apply_skin_tone("🔥", "medium"))        # 🔥  (unchanged, not a base)
print(strip_skin_tone("👍🏾"))               # 👍

JavaScript Implementation

const SKIN_TONE_MODIFIERS = {
  light:       '\u{1F3FB}',  // 🏻
  mediumLight: '\u{1F3FC}',  // 🏼
  medium:      '\u{1F3FD}',  // 🏽
  mediumDark:  '\u{1F3FE}',  // 🏾
  dark:        '\u{1F3FF}',  // 🏿
};

const MODIFIER_REGEX = /[\u{1F3FB}-\u{1F3FF}]/gu;

function stripSkinTone(emoji) {
  return emoji.replace(MODIFIER_REGEX, '');
}

function applySkinTone(emoji, tone) {
  const base = stripSkinTone(emoji);
  const modifier = SKIN_TONE_MODIFIERS[tone];
  if (!modifier) return base;

  // Check if base accepts modifiers (simplified check using known bases)
  if (!/^\p{Emoji_Modifier_Base}/u.test(base)) return base;

  return base + modifier;
}

console.log(applySkinTone("👋", "medium"));        // 👋🏽
console.log(applySkinTone("👋🏻", "dark"));        // 👋🏿
console.log(stripSkinTone("👍🏾"));               // 👍

Multi-Person ZWJ Sequences and Skin Tones

Person emoji combined with ZWJ (like 👫 couple or 🤝 handshake) support independent skin tones per person. These are encoded as separate modifier bases joined by ZWJ, each with their own modifier:

👫 (couple) = 👩 + ZWJ + 👨
👩🏽‍🤝‍👨🏿 = 👩🏽 + ZWJ + 🤝 + ZWJ + 👨🏿

Parsing Multi-Person Sequences

def parse_emoji_components(zwj_sequence: str) -> list[str]:
    """
    Split a ZWJ sequence into its constituent emoji components.
    Returns list of emoji (without ZWJ characters).
    """
    ZWJ = "\u200D"
    return zwj_sequence.split(ZWJ)

def apply_skin_tones_to_zwj(sequence: str, tones: list[str]) -> str:
    """
    Apply skin tones to each person component in a ZWJ sequence.
    tones: list of tone names, one per modifiable component.
    """
    ZWJ = "\u200D"
    components = parse_emoji_components(sequence)
    tone_iter = iter(tones)
    result = []

    for component in components:
        if is_modifier_base(component) or is_modifier_base(strip_skin_tone(component)):
            tone = next(tone_iter, None)
            if tone:
                result.append(apply_skin_tone(strip_skin_tone(component), tone))
            else:
                result.append(strip_skin_tone(component))
        else:
            result.append(component)

    return ZWJ.join(result)

# Example: Two-person handshake with different skin tones
handshake_zwj = "🧑\u200D🤝\u200D🧑"
result = apply_skin_tones_to_zwj(handshake_zwj, ["medium", "dark"])
print(result)  # 🧑🏽‍🤝‍🧑🏿

UI Implementation Patterns

Skin Tone Picker Component (React)

const TONES = [
  { id: null,          label: "Default",      swatch: "👋" },
  { id: "1F3FB",      label: "Light",        swatch: "👋🏻" },
  { id: "1F3FC",      label: "Medium-Light", swatch: "👋🏼" },
  { id: "1F3FD",      label: "Medium",       swatch: "👋🏽" },
  { id: "1F3FE",      label: "Medium-Dark",  swatch: "👋🏾" },
  { id: "1F3FF",      label: "Dark",         swatch: "👋🏿" },
];

function SkinTonePicker({ value, onChange }) {
  return (
    <div role="radiogroup" aria-label="Skin tone">
      {TONES.map(tone => (
        <button
          key={tone.id ?? 'default'}
          role="radio"
          aria-checked={value === tone.id}
          aria-label={tone.label}
          onClick={() => onChange(tone.id)}
          className={`skin-tone-btn ${value === tone.id ? 'selected' : ''}`}
        >
          {tone.swatch}
        </button>
      ))}
    </div>
  );
}

Applying the Selected Tone to Emoji Grid

function getEmojiWithTone(emojiData, skinToneModifier) {
  if (!skinToneModifier) return emojiData.emoji;

  // Use pre-computed skin variations from the dataset
  const variation = emojiData.skinVariations?.[skinToneModifier];
  if (!variation) return emojiData.emoji;

  return String.fromCodePoint(
    ...variation.unified.split('-').map(h => parseInt(h, 16))
  );
}

Persisting Skin Tone Preferences

Store the user's chosen skin tone in localStorage or a user profile:

const STORAGE_KEY = 'emoji-skin-tone';

function useSkinTone() {
  const [tone, setTone] = useState(() => {
    try {
      return localStorage.getItem(STORAGE_KEY) || null;
    } catch {
      return null;
    }
  });

  const updateTone = useCallback((newTone) => {
    setTone(newTone);
    try {
      if (newTone) localStorage.setItem(STORAGE_KEY, newTone);
      else localStorage.removeItem(STORAGE_KEY);
    } catch {
      // Ignore storage errors
    }
  }, []);

  return [tone, updateTone];
}

Edge Cases

  • Non-base emoji: Applying a modifier to 🔥, ❤️, or other non-base emoji should be a no-op — return the original.
  • Already-modified emoji: Strip the existing modifier before applying a new one to avoid double modifiers (👋🏻🏽 is invalid).
  • ZWJ sequence order: Modifiers must immediately follow the base — a ZWJ between base and modifier breaks the sequence.
  • Handshake (🤝): U+1F91D is itself an Emoji_Modifier_Base in newer Unicode versions, enabling toned handshake sequences.

Explore More on EmojiFYI

Herramientas relacionadas

🔀 Comparar plataformas Comparar plataformas
Compara cómo se muestran los emojis en Apple, Google, Samsung, Microsoft y más. Ve las diferencias visuales en paralelo.
🔍 Analizador de secuencias Analizador de secuencias
Decodifica secuencias ZWJ, modificadores de tono de piel, secuencias de teclas y pares de banderas en sus componentes individuales.

Términos del glosario

Conector de ancho cero (ZWJ) Conector de ancho cero (ZWJ)
Carácter Unicode invisible (U+200D) utilizado para unir varios emoji en un único emoji compuesto, como la combinación de personas y objetos en emoji de profesiones.
Emoji Emoji
Palabra japonesa (絵文字) que significa 'carácter imagen' — pequeños símbolos gráficos usados en la comunicación digital para expresar ideas, emociones y objetos.
Modificador de tono de piel Modificador de tono de piel
Cinco caracteres modificadores Unicode basados en la escala Fitzpatrick que cambian el color de piel de los emoji humanos (U+1F3FB a U+1F3FF).
Punto de código Punto de código
Valor numérico único asignado a cada carácter en el estándar Unicode, escrito en el formato U+XXXX (por ejemplo, U+1F600 para 😀).
Unicode Unicode
Estándar universal de codificación de caracteres que asigna un número único a cada carácter de todos los sistemas de escritura y conjuntos de símbolos, incluidos los emoji.

Artículos relacionados