# Why Character Counters Lie: Graphemes, SMS & X Rules

Most character counters disagree with each other — and with the platform you are posting to. The reason is not a bug: "character" means four different things depending on who is counting. Here is the map.

## Four units, one word

1. **Code units** — what JavaScript's `.length` counts (UTF-16). `"👨‍👩‍👧‍👦".length === 11`.
2. **Code points** — Unicode scalar values. The family emoji is 7.
3. **Grapheme clusters** — what a human sees. The family emoji is 1. A flag is 1. `é` built from `e` + combining acute is 1.
4. **Weighted units** — what X counts. URLs are 23, some ranges weigh 2.

A counter is only "right" relative to the unit your destination uses. Mismatch the unit and you get the classic failures: a form that rejects text that fits, or accepts text the platform then truncates.

## Graphemes: the only unit readers care about

ZWJ sequences, regional indicators, combining marks, emoji modifiers — all of these assemble multiple code points into one visible glyph. The browser-native way to count them:

```js
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
const graphemes = [...segmenter.segment(text)].length;
```

Any serious counter should default to graphemes. Code-unit counts are an implementation detail leaking into UX.

## X: the weighted regime

X (Twitter) allows 280 weighted characters per standard post. The [twitter-text](https://github.com/twitter/twitter-text) configuration assigns weights per range: most characters weigh 1, URLs are a flat **23** regardless of actual length, and emoji/wide ranges weigh 2. A 193-character draft with a 47-character link and one emoji lands around **169 weighted** — the link collapses, the emoji doubles. If your validation uses a plain count, it is validating against a limit that does not exist.

## SMS: the segment regime

SMS billing counts **segments**, sized by encoding: GSM-7 gives 160 characters per single message (153 when concatenated); UCS-2 — triggered by a single emoji or CJK character — drops that to 70 (67 concatenated). The GSM-7 extension table (`€ ^ { } \ [ ] ~ |`) costs two units per character. One emoji does not just add a character; it re-encodes the entire message and more than halves its capacity.

## The tool

Disclosure: I built [toolkitloop.com/character-counter](https://toolkitloop.com/character-counter), one of 30 free browser tools I maintain. It counts graphemes by default, has an X mode applying the weighted URL rule, reports with/without spaces, and runs entirely client-side — your draft never leaves the browser. A companion [social media character limits guide](https://toolkitloop.com/social-media-character-limits) keeps the per-field numbers in one place.

The broader lesson: whenever you build around a "character limit," find out which of the four units the destination counts — then count that. Everything else is a guess wearing a number's clothes.
