An emoji often counts as 2 characters because JavaScript, Java, C# and the HTML maxlength attribute measure text in UTF-16 code units, and most emoji sit outside the range one code unit can hold, so each takes two. Other systems count differently: Python counts code points, PHP's strlen counts bytes, and a person counts what they see, which Unicode calls a grapheme cluster. The thumbs-up with a medium skin tone, 👍🏽, is 1 grapheme, 2 code points, 4 UTF-16 code units and 8 UTF-8 bytes.
You can see the UTF-16 count for yourself: paste 👍🏽 into the FindUtils Character Counter, which counts the way JavaScript does, and it shows 4.
Four Ways to Measure the Same Text
| Unit | What it counts | 👍🏽 | Who counts this way |
|---|---|---|---|
| Grapheme cluster | What a reader sees as one character | 1 | Intl.Segmenter, PHP grapheme_strlen, Swift String.count |
| Code point | Unicode's numbered characters | 2 | Python len, PHP mb_strlen, MySQL CHAR_LENGTH, PostgreSQL length |
| UTF-16 code unit | 16-bit pieces of the UTF-16 encoding | 4 | JavaScript .length, HTML maxlength, Java, C#, SQL Server nvarchar(n) |
| UTF-8 byte | 8-bit pieces of the UTF-8 encoding | 8 | PHP strlen, MySQL LENGTH, file and request sizes |
Code points. Unicode gives every character a number. 👍 is U+1F44D, and the medium skin tone is a separate code point, U+1F3FD, a modifier that changes the emoji before it. So 👍🏽 is two code points.
UTF-16 code units. UTF-16 stores code points up to U+FFFF in one 16-bit unit. Anything above that, which includes most emoji, is stored as a surrogate pair of two units. Each of the two code points in 👍🏽 needs a pair, so the total is 4. The ECMAScript specification defines a string as a sequence of 16-bit values, and length is how many there are.
UTF-8 bytes. UTF-8 uses 1 byte for ASCII, 2 for most accented Latin letters, 3 for most Chinese, Japanese and Korean characters, and 4 for emoji. Two emoji code points make 8 bytes. To see the bytes, put 👍 into the URL Encoder: it becomes %F0%9F%91%8D, one % group per byte.
Grapheme clusters. Unicode Standard Annex #29 defines the "user-perceived character": a base character plus anything attached to it, such as a skin-tone modifier, a combining accent or emoji joined by a zero-width joiner. That is the count most people mean when they say "characters".
Measured: Eight Strings, Five Counts
We measured these strings on October 24, 2026 with Node.js (.length, [...s].length, Intl.Segmenter and Buffer.byteLength) and with X's open-source counting library, twitter-text 3.
| Text | Graphemes | Code points | UTF-16 units | UTF-8 bytes | X count |
|---|---|---|---|---|---|
A | 1 | 1 | 1 | 1 | 1 |
é (one code point, U+00E9) | 1 | 1 | 1 | 2 | 1 |
é (e + combining accent U+0301) | 1 | 2 | 2 | 3 | 1 |
中 | 1 | 1 | 1 | 3 | 2 |
| 👍 | 1 | 1 | 2 | 4 | 2 |
| 👍🏽 | 1 | 2 | 4 | 8 | 2 |
| 👨👩👧 (family: 3 people, 2 joiners) | 1 | 5 | 8 | 18 | 2 |
| 🇹🇷 (flag: 2 regional indicator letters) | 1 | 2 | 4 | 8 | 2 |
Two rows deserve a second look. The two forms of é look identical, but one is a single code point and the other is e followed by a combining accent, so the code point, UTF-16 and byte counts differ. Text copied from some apps and documents uses the second form, so two strings that look the same can have different lengths. And the family emoji is one picture made of five code points, which is why it can count as 8 in a JavaScript form.
Who Counts What
- JavaScript:
"👍🏽".lengthis 4.[..."👍🏽"].lengthiterates by code point and gives 2.Intl.Segmenterwithgranularity: "grapheme"gives 1, and works in all current major browsers. - HTML forms: the HTML Standard measures
maxlengthandminlengthin code units, the same as.length, with each line break in a<textarea>counted as one character. - Python:
len("👍🏽")is 2. Python 3 strings are sequences of code points. - PHP:
strlenreturns bytes (8),mb_strlenwith UTF-8 returns code points (2), andgrapheme_strlenfrom the intl extension returns graphemes (1). - MySQL:
VARCHAR(n)limits characters, which here means code points, andCHAR_LENGTHcounts them;LENGTHcounts bytes. Emoji need theutf8mb4character set: the olderutf8mb3stores at most 3 bytes per character and cannot hold them. - PostgreSQL:
varchar(n)stores up to n characters (code points), not bytes. - SQL Server: in
nvarchar(n), Microsoft's documentation says n is a number of byte-pairs, not characters, so an emoji takes two of the n. - X (Twitter): X weights characters. Most Latin, Cyrillic and Greek letters count 1, Chinese, Japanese and Korean characters count 2, every emoji counts 2 however many code points it has, and every link counts 23, against a limit of 280.
- SMS: a single message holds 160 characters of the GSM-7 alphabet. One character outside it, such as an emoji or a curly quote, switches the whole message to an encoding that holds 70 per message, and long messages are split into parts of 153 or 67.
Where the Mismatch Bites
A form that the browser accepts and the server rejects. A <textarea maxlength="280"> counts code units, and a line break counts as one. But when a classic HTML form is submitted, the HTML Standard converts every line break to CR LF, two characters. A 280-character message with five line breaks arrives at the server as 285 characters, and a server-side "maximum 280" rule refuses text the browser allowed. Requests sent by JavaScript as JSON keep a single line feed, so the same form can behave differently depending on how it is submitted.
A form that the server accepts and the browser blocks. The opposite also happens. Laravel's max:280 rule measures a string with mb_strlen, which counts code points, while the browser's maxlength counts code units. Emoji-heavy text hits the browser limit first: 70 thumbs-up with a skin tone is 140 code points but 280 code units, so the browser stops at 70 while the server would accept up to 140.
A database that cuts text off or rejects it. A column sized in bytes, or nvarchar sized in byte-pairs, fills up sooner than a character limit suggests. In MySQL with utf8mb3, an emoji is not stored at all: depending on the SQL mode, the insert fails or the text is cut at the emoji.
A truncation that breaks a character. Cutting a string at a code-unit position, as text.slice(0, 160) does, can split a surrogate pair and leave half an emoji, which displays as a replacement character, or split a flag into a single letter. The Text Truncate tool counts in code units like the browser, but moves each cut back to the start of the character, so an emoji, a flag or an accented letter is never split.
How to Keep a Limit Consistent
- Decide what the limit means. "What the user sees" is graphemes. "What fits in the column" is the unit your database counts. They are different numbers, and the product rule should say which one it means.
- Count the same way on both sides. If the server counts code points with
mb_strlen, count code points in the browser too ([...value].length) and show that number, rather than relying onmaxlengthalone. - Normalize line breaks before counting. Convert CR LF to LF on the server before you check a length, so a classic form post and a JSON request are measured the same way.
- Normalize Unicode when it matters. Converting text to NFC (
normalize("NFC")in JavaScript,Normalizer::normalizein PHP) turnse+ combining accent into the singleé, so the same visible text always has the same length. X's library does this before it counts, which is why both forms ofécount 1 there. - Size columns for the worst case. For a limit of n code points in UTF-8, plan for up to 4n bytes; in
nvarchar, up to 2n byte-pairs. - Cut on character boundaries. Use
Intl.Segmenterin JavaScript orgrapheme_substrin PHP, or check the cut with Text Truncate before you hard-code an expected value in a test.
Check a Length With FindUtils
- Character Counter: counts characters the way JavaScript and
maxlengthdo, plus words, sentences and lines. Its bars for X, Instagram, LinkedIn and others compare that count with each platform's number, so for text with emoji, links or Chinese, Japanese or Korean characters, treat them as a guide and check where you post. - Text Truncate: cuts text to a limit without splitting an emoji, a flag or an accented letter, and shows the character count before and after.
- URL Encoder: shows the UTF-8 bytes of any character as
%XXgroups, which is the quickest way to see a byte count. - Emoji Finder: search and copy an emoji to test how your own form or database counts it.
Need help with a project? The team behind FindUtils takes on freelance work, such as a Laravel Livewire app or admin panel with dynamic pages and forms.
FAQ
Why does JavaScript say an emoji has length 2?
JavaScript strings are sequences of UTF-16 code units, and .length counts those units. Emoji are above U+FFFF, so each needs a surrogate pair of two units. Emoji with a skin tone or several joined people need more: 👍🏽 is 4 and 👨👩👧 is 8.
How do I count characters the way a person sees them?
Count grapheme clusters. In JavaScript, use [...new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(text)].length. In PHP, use grapheme_strlen. Both count 👍🏽 and 🇹🇷 as 1.
Does HTML maxlength count emoji as 1 or 2?
As 2 for most single emoji. The HTML Standard measures maxlength in UTF-16 code units, the same as JavaScript's .length. Line breaks in a <textarea> count as one each.
Why does PHP strlen give a bigger number than mb_strlen?
strlen counts bytes and mb_strlen counts characters (code points). In UTF-8, any character outside ASCII takes 2 to 4 bytes, so the byte count is higher whenever the text has an accent, a non-Latin letter or an emoji.
How many characters does an emoji use on X?
Two, regardless of how many code points it is made of. X's documentation says all emoji count as 2, including ones with a skin tone and ones joined with zero-width joiners.
Can I store emoji in a MySQL VARCHAR column?
Yes, if the column uses the utf8mb4 character set. The older utf8mb3 (long called just utf8) stores at most 3 bytes per character and cannot store emoji, which need 4.
Related Reading
Sources
- Unicode Standard Annex #29: Unicode Text Segmentation
- ECMAScript Language Specification: The String Type
- MDN: Intl.Segmenter
- HTML Standard: the maxlength attribute
- HTML Standard: converting form entries to name-value pairs
- PHP manual: strlen, mb_strlen and grapheme_strlen
- MySQL 8.4 Reference Manual: The utf8mb4 Character Set
- PostgreSQL documentation: Character Types
- Microsoft Learn: nchar and nvarchar (Transact-SQL)
- X Developer Platform: Counting characters
- Twilio: How long can a message be?