Implementing Skin Tone Modifiers Programmatically
Skin tone modifiers let users personalize emojiEmoji
Từ tiếng Nhật (絵文字) có nghĩa là 'ký tự hình ảnh' — các ký hiệu đồ họa nhỏ dùng trong giao tiếp kỹ thuật số để diễn đạt ý tưởng, cảm xúc và sự vật. to match their appearance. What looks like a single character — 👋🏽 — is actually two UnicodeUnicode
Tiêu chuẩn mã hóa ký tự phổ quát gán một số duy nhất cho mỗi ký tự trong tất cả hệ thống chữ viết và bộ ký hiệu, bao gồm cả emoji. code points: a modifier base (👋 U+1F44B) followed by a skin tone modifierSkin Tone Modifier
Năm ký tự điều chỉnh Unicode dựa trên thang Fitzpatrick, thay đổi màu da của emoji người (từ U+1F3FB đến 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)
Ký tự Unicode vô hình (U+200D) dùng để ghép nhiều emoji thành một emoji tổng hợp, chẳng hạn kết hợp người và vật thể thành emoji nghề nghiệp. 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_Basein newer Unicode versions, enabling toned handshake sequences.
Explore More on EmojiFYI
- See skin tone variants for any emoji: Sequence Analyzer
- Compare skin tone rendering across platforms: Compare Tool
- Unicode modifier properties explained: Glossary
- Query skin tone variant data: API Reference