๐Ÿ› ๏ธ Technical & Developer

Implementing Skin Tone Modifiers Programmatically

Implementing Skin Tone Modifiers Programmatically

Skin tone modifiers let users personalize emojiEmoji
A Japanese word (็ตตๆ–‡ๅญ—) meaning 'picture character' โ€” small graphical symbols used in digital communication to express ideas, emotions, and objects.
to match their appearance. What looks like a single character โ€” ๐Ÿ‘‹๐Ÿฝ โ€” is actually two UnicodeUnicode
Universal character encoding standard that assigns a unique number to every character across all writing systems and symbol sets, including emoji.
code points: a modifier base (๐Ÿ‘‹ U+1F44B) followed by a skin tone modifierSkin Tone Modifier
Five Unicode modifier characters based on the Fitzpatrick scale that change the skin color of human emoji (U+1F3FB to U+1F3FF).
(๐Ÿฝ U+1F3FD). Implementing this correctly requires understanding which emoji accept modifiers, how to apply and strip them, and how to handle complex multi-person ZWJZero Width Joiner (ZWJ)
An invisible Unicode character (U+200D) used to join multiple emoji into a single composite emoji, such as combining people and objects into profession emoji.
sequences.

The Fitzpatrick Scale

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

Modifier Code PointCode Point
A unique numerical value assigned to each character in the Unicode standard, written in the format U+XXXX (e.g., U+1F600 for ๐Ÿ˜€).
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

Related Tools

๐Ÿ”€ Platform Compare Platform Compare
Compare how emojis render across Apple, Google, Samsung, Microsoft, and more. See visual differences side by side.
๐Ÿ” Sequence Analyzer Sequence Analyzer
Decode ZWJ sequences, skin tone modifiers, keycap sequences, and flag pairs into individual components.

Glossary Terms

Code Point Code Point
A unique numerical value assigned to each character in the Unicode standard, written in the format U+XXXX (e.g., U+1F600 for ๐Ÿ˜€).
Emoji Emoji
A Japanese word (็ตตๆ–‡ๅญ—) meaning 'picture character' โ€” small graphical symbols used in digital communication to express ideas, emotions, and objects.
Skin Tone Modifier Skin Tone Modifier
Five Unicode modifier characters based on the Fitzpatrick scale that change the skin color of human emoji (U+1F3FB to U+1F3FF).
Unicode Unicode
Universal character encoding standard that assigns a unique number to every character across all writing systems and symbol sets, including emoji.
Zero Width Joiner (ZWJ) Zero Width Joiner (ZWJ)
An invisible Unicode character (U+200D) used to join multiple emoji into a single composite emoji, such as combining people and objects into profession emoji.

Related Stories