Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

Why "πŸ‘¨β€πŸ‘©β€πŸ‘§".length Is 8, and How to Count What Users See

C
Carlo Fontanos
· 2 min read

A user sets their display name to "Ana πŸ‘¨β€πŸ‘©β€πŸ‘§" and your 20-character truncation renders "Ana πŸ‘¨β€πŸ‘©β€οΏ½". The corpse of half an emoji, live in production. The root cause: .length doesn't count what anyone sees.

'πŸ‘¨β€πŸ‘©β€πŸ‘§'.length;         // 8 - eight UTF-16 code units
[...'πŸ‘¨β€πŸ‘©β€πŸ‘§'].length;     // 3 - three code points (better, still wrong)
// What a human sees: 1 character

That family emoji is three emoji glued with zero-width joiners. slice() cuts code units, spread cuts code points - both can split inside the glue. The unit users perceive is the grapheme cluster, and only one API speaks it:

const seg = new Intl.Segmenter(undefined, { granularity: 'grapheme' });

function truncate(str, max) {
    const graphemes = [...seg.segment(str)];
    if (graphemes.length <= max) return str;
    return graphemes.slice(0, max).map(s => s.segment).join('') + '…';
}

truncate('Ana πŸ‘¨β€πŸ‘©β€πŸ‘§ GarcΓ­a', 6);   // "Ana πŸ‘¨β€πŸ‘©β€πŸ‘§β€¦" - the family survives intact

segment() returns an iterable of {segment, index} objects; joining the segments back reconstructs the original exactly. This also handles the less flashy cases that bite real text: accented characters composed from combining marks ("é" as e + ́), flag emojis, skin-tone modifiers, Devanagari conjuncts.

Word counting that respects languages

const words = new Intl.Segmenter('en', { granularity: 'word' });

function wordCount(text) {
    return [...words.segment(text)].filter(s => s.isWordLike).length;
}

The isWordLike flag separates actual words from punctuation and spaces. The killer detail: split(/\s+/) counts zero words in Japanese or Thai, which don't use spaces - Segmenter with the right locale segments them correctly. If your CMS shows word counts or reading time on user content, this is the difference between a feature and a lie. (My own reading-time estimates run on exactly this.)

Sentence granularity, and what Segmenter is not

granularity: 'sentence' gives you first-sentence extraction for meta descriptions and previews - far more robust than splitting on ". " (which mangles "Dr. Smith" and "v2.5"). It's not a full NLP tokenizer, though: no part-of-speech tags, no hyphenation decisions, and sentence rules are heuristic. For UI-level text handling, that's plenty.

Support and the fallback

Chrome 87+, Safari 14.1+, Node 16+ - Firefox was last, landing in 125 (2024), so it's now safely universal for current browsers. For older targets, feature-detect and fall back to code-point spread - wrong for edge cases but strictly better than .length slicing:

const graphemes = 'Segmenter' in Intl
    ? [...new Intl.Segmenter().segment(str)].map(s => s.segment)
    : [...str];

Once you've seen the .length lie, you spot it everywhere: character counters, database-length validations that disagree with the UI, avatar initials made of half a surrogate pair. Count graphemes, and users can put their families in their names in peace.

C
Written by Carlo Fontanos

Full-stack web developer sharing practical tutorials and building tools that ship.

Got something on your mind?

My inbox is open - no forms disappearing into the void here.

  • Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
  • Hire me for a project Have something custom in mind? Let's talk scope and timelines.
  • Product support Bought something here? I'll help you get it running.

I usually reply within 1-2 business days.

Message sent!

Your details are only used to reply to you.

Keep reading