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

UnitWhat it counts👍🏽Who counts this way
Grapheme clusterWhat a reader sees as one character1Intl.Segmenter, PHP grapheme_strlen, Swift String.count
Code pointUnicode's numbered characters2Python len, PHP mb_strlen, MySQL CHAR_LENGTH, PostgreSQL length
UTF-16 code unit16-bit pieces of the UTF-16 encoding4JavaScript .length, HTML maxlength, Java, C#, SQL Server nvarchar(n)
UTF-8 byte8-bit pieces of the UTF-8 encoding8PHP 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.

TextGraphemesCode pointsUTF-16 unitsUTF-8 bytesX count
A11111
é (one code point, U+00E9)11121
é (e + combining accent U+0301)12231
中11132
👍11242
👍🏽12482
👨‍👩‍👧 (family: 3 people, 2 joiners)158182
🇹🇷 (flag: 2 regional indicator letters)12482

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: "👍🏽".length is 4. [..."👍🏽"].length iterates by code point and gives 2. Intl.Segmenter with granularity: "grapheme" gives 1, and works in all current major browsers.
  • HTML forms: the HTML Standard measures maxlength and minlength in 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: strlen returns bytes (8), mb_strlen with UTF-8 returns code points (2), and grapheme_strlen from the intl extension returns graphemes (1).
  • MySQL: VARCHAR(n) limits characters, which here means code points, and CHAR_LENGTH counts them; LENGTH counts bytes. Emoji need the utf8mb4 character set: the older utf8mb3 stores 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

  1. 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.
  2. 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 on maxlength alone.
  3. 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.
  4. Normalize Unicode when it matters. Converting text to NFC (normalize("NFC") in JavaScript, Normalizer::normalize in PHP) turns e + 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.
  5. 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.
  6. Cut on character boundaries. Use Intl.Segmenter in JavaScript or grapheme_substr in 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 maxlength do, 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 %XX groups, 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.

Sources