Implementing Skin Tone Modifiers Programmatically

Implementing Skin Tone Modifiers Programmatically

Skin tone modifiers let users personalize emojiEmoji
Ein japanisches Wort (็ตตๆ–‡ๅญ—) mit der Bedeutung 'Bildzeichen' โ€” kleine grafische Symbole in der digitalen Kommunikation zum Ausdrรผcken von Ideen, Gefรผhlen und Objekten.
to match their appearance. What looks like a single character โ€” ๐Ÿ‘‹๐Ÿฝ โ€” is actually two UnicodeUnicode
Universeller Zeichenkodierungsstandard, der jedem Zeichen aller Schriftsysteme und Symbolsรคtze einschlieรŸlich Emoji eine eindeutige Zahl zuweist.
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 ZWJBreitenloser Verbinder (ZWJ)
Ein unsichtbares Unicode-Zeichen (U+200D), das verwendet wird, um mehrere Emoji zu einem zusammengesetzten Emoji zu verbinden, etwa beim Kombinieren von Personen und Objekten zu Berufs-Emoji.
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

Verwandte Werkzeuge

๐Ÿ”€ Plattformvergleich Plattformvergleich
Vergleichen Sie, wie Emojis bei Apple, Google, Samsung, Microsoft und weiteren Plattformen dargestellt werden. Sehen Sie visuelle Unterschiede im direkten Vergleich.
๐Ÿ” Sequenzanalysator Sequenzanalysator
Entschlรผsseln Sie ZWJ-Sequenzen, Hautton-Modifikatoren, Tasten-Sequenzen und Flaggenpaare in einzelne Komponenten.

Glossar-Begriffe

Breitenloser Verbinder (ZWJ) Breitenloser Verbinder (ZWJ)
Ein unsichtbares Unicode-Zeichen (U+200D), das verwendet wird, um mehrere Emoji zu einem zusammengesetzten Emoji zu verbinden, etwa beim Kombinieren von Personen und Objekten zu Berufs-Emoji.
Codepoint Codepoint
Ein eindeutiger numerischer Wert, der jedem Zeichen im Unicode-Standard zugewiesen wird und im Format U+XXXX angegeben wird (z. B. U+1F600 fรผr ๐Ÿ˜€).
Emoji Emoji
Ein japanisches Wort (็ตตๆ–‡ๅญ—) mit der Bedeutung 'Bildzeichen' โ€” kleine grafische Symbole in der digitalen Kommunikation zum Ausdrรผcken von Ideen, Gefรผhlen und Objekten.
Hautton-Modifikator Hautton-Modifikator
Fรผnf Unicode-Modifikatorzeichen auf Basis der Fitzpatrick-Skala, die die Hautfarbe menschlicher Emoji verรคndern (U+1F3FB bis U+1F3FF).
Unicode Unicode
Universeller Zeichenkodierungsstandard, der jedem Zeichen aller Schriftsysteme und Symbolsรคtze einschlieรŸlich Emoji eine eindeutige Zahl zuweist.

Verwandte Artikel