A form field that says “280 characters left” is only as honest as the count behind it. A writer types an emoji, a family of people, or an accented letter, and the counter disagrees with what the platform accepts. The count can be too high, so the writer is blocked too early, or too low, so the post is rejected at the last step. Both come from counting the wrong unit.
Count what the platform counts
Different limits use different units. A plain string.length counts UTF-16 code units, which is what JavaScript stores. A user-visible character is a grapheme cluster, which may be several code units long. Some platforms count code points, some count bytes in a given encoding, and some apply their own rules. Find out which unit the destination uses, and count in that unit. A counter that is wrong for the destination is worse than no counter.
const text = "e\u0301\u{1F468}\u200D\u{1F469}"; // accented e, then a family emoji
text.length; // 6: UTF-16 code units
[...text].length; // 4: code points
[...new Intl.Segmenter().segment(text)].length; // 2: user-perceived charactersThe three numbers differ for the same text. A writer sees two characters. A system that counts code points sees four, and one that counts UTF-16 units sees six. Pick the unit the destination uses, and name it in the counter’s help text, so a writer who gets a rejection knows why.
SMS adds a second limit: encoding
A plain text SMS can hold 160 characters from the GSM alphabet. One character outside that set, such as a curly quote or an emoji, switches the whole message to UCS-2 encoding, which holds 70 characters. The counter must show the switch, because the same message can grow from one segment to three when a writer adds a single emoji. Show the segment count and the encoding, and update both as the writer types.
Count on input, enforce on submit
A hard limit that stops typing at the boundary feels punitive, and it breaks paste. A writer who pastes a paragraph into a 280-character field sees the paste cut off mid-word. Prefer a counter that turns red past the limit, and enforce the limit when the post is submitted. If text must be cut, cut at a grapheme boundary, never inside an emoji sequence, which would leave a broken symbol behind.
The server counts too, with the same rules
A client-side counter is advice, not validation. The server must check the length with the same unit the destination uses, because a request can come from anywhere. Keep the rule in one function that both the counter and the server call. A mismatch between the two is the bug where the counter says the post is fine and the server returns an error with no explanation.
A short test list
- Type a family emoji. The counter should count it as one character, not four or five.
- Type an accented letter in a 160-character SMS draft and check the counter switches to the 70-character limit.
- Paste a long paragraph into a limited field and confirm the paste is cut at a grapheme boundary.
- Submit a post exactly at the limit, then one character over. The server should accept the first and reject the second with a clear message.
- Compare the counter with the server’s count for a text containing combining marks.
RichTextEditor is a perpetual-licence JavaScript editor — one purchase, self-hosted, no metered editor loads. Download the evaluation or see how it compares.