Blank text message: the bubble stays empty for a space, and a character that draws no ink sends anyway

Short answer: a blank text message works because the send button never asks “is there a message”. It asks “is there anything in the box that a trim would not remove”. Measured in Chromium 153, an empty chat bubble is 16 px tall, a bubble holding one letter is 38.39 px, and a bubble holding one ordinary space is 16 px again. All 23 invisible characters tested take the bubble to 38.39 px, but only 16 of 23 get past the emptiness check, and the seven that do not are exactly the seven trim() deletes. The character most often recommended for this job, U+3164, is also the one that reserves a full em of width.

Refused by the send button, for contrast:

One character each. The first three draw no ink and advance the cursor by nothing, so the bubble contains a message and shows no width for it. The fourth also survives the emptiness check and also draws no ink, but it advances a full em — 64 px at a 64 px font — so ten of them make a bubble 184 px wide. U+00A0 is the control: it advances 19 px, draws nothing, and trim() removes it, so it looks like a blank message and is refused like a space.

Everything below was measured, not looked up: Chromium 153.0.8010.12 via Playwright for the bubble, the canvas, the field types and the form submission, plus Node 22.22.2 for the string checks.

Two gates, and an ordinary space fails both

Two separate things stand between you and a blank message. The first is layout: whether the renderer has anything to draw. The second is the string check behind the send button. A space fails the first because a whitespace-only text node produces no line box, and it fails the second because a trim deletes it.

In the bubbleHeightWhat the renderer did
nothing at all16.00 pxpadding only
one ordinary letter38.39 pxone line box
one ordinary space16.00 pxpadding only — nothing to lay out
any of the 23 invisible characters38.39 pxone line box, same as a letter

So a space does not disappear; it is stored and skipped at layout time. The check behind the button is the obvious one: refuse the message if the field value is empty after trimming.

Value in the fieldPasses the trim() checkWhich characters
an ordinary spaceno—
23 invisible characters16 of 23everything except the seven below
the seven that fail0 of 7U+00A0, U+2028, U+2029, U+202F, U+205F, U+3000, U+FEFF

The seven that fail are exactly the code points JavaScript's trim() removes, so the split is not “invisible” against “visible” but “whitespace to the language” against “not” — and seven of the characters handed out for this job are on the wrong side of it. U+00A0 is the one to watch: the old advice for a blank message was a no-break space, and it advances 19 px, so it looks like it worked while the button refuses it. People meet this without knowing why. The clearest account of the behaviour is someone whose handset would not show a send button until they typed characters and not just a space, while blank messages kept arriving from a contact who had not sent any.

The placeholder is the one signal that lies to you

The only feedback a message box gives you is the placeholder text disappearing, and that signal is not the one you want. A placeholder hides whenever the value is not the empty string, and one ordinary space is not the empty string. Measured in Chromium 153 with :placeholder-shown, the placeholder is gone for a space, and the trim-based check still refuses it.

Value in the boxPlaceholder still shownSend button would refuse
empty stringyesyes
one ordinary spacenoyes
U+00A0, U+202F, U+205F, U+3000, U+FEFFnoyes
U+200B, U+2060, U+00AD, U+3164nono

So the placeholder vanishing says the DOM has a text node, and nothing about whether the app will send. Five of the characters behave exactly like a space on both rows: the box looks occupied, the button stays dead, and there is no visible reason why. A better test is to type a second character and delete it — if the button lights up and stays lit, something is in there. One length rule points the other way: measured on a required field, all 23 characters count as filled in, including the seven whose trim() length is zero, while the empty string counts as missing.

Empty message copy paste: the characters worth copying, and the one to skip

An invisible character does two jobs at once and they come apart: it can draw nothing, and it can advance the cursor by nothing. The characters people hand out are usually invisible in the first sense and not in the second, which is why a blank message so often arrives with a gap in it. Drawing each character to a canvas at 64 px and counting the pixels that are not the background separates the two.

CharacterInk drawnAdvanceWhat it does in a bubble
an ordinary letter1066 px63 pxa visible character
an ordinary space0 px19 pxa gap, and the button refuses it
U+200B zero width space0 px0 pxnothing, and it sends
U+2060 word joiner0 px0 pxnothing, and it sends
U+00AD soft hyphen0 px0 pxnothing, and it sends
U+200C, U+200D, U+200E, U+200F0 px0 pxnothing, and they send
U+034F, U+061C, U+180E, U+E0001, U+E00200 px0 pxnothing, and they send
U+202F narrow no-break space0 px10 pxa thin gap, and it is refused
U+205F medium math space0 px14 pxa gap, and it is refused
U+00A0 no-break space0 px19 pxa gap the size of a space, refused
U+3000 ideographic space0 px64 pxa full em of gap, refused
U+3164 hangul filler0 px64 pxa full em of gap, and it sends
U+FFA0 halfwidth hangul filler0 px32 pxhalf an em of gap, and it sends

The last three rows are the ones that get recommended and the ones that leave a mark. U+3164 Hangul Filler is the most common answer to “how do I send a blank message”, and measured it reserves a full em: 64 px at a 64 px font, and the same 16 px cell a chat bubble uses at 16 px. Ten copies of it occupy 184 px of bubble. It draws no ink, and it is not blank in the sense that a reader can see how much of it there is.

None of the 23 drew any ink in the font stack measured here, which is worth stating because the usual description of these characters on a phone is a box. A box is what you get when the font has no glyph for the code point at all. The advance is a property of the character, the box is a property of the device, and only the first one travels with your message.

Invisible message copy paste: what the other person actually sees

Nothing is drawn, and the bubble is still not the size of an empty one. Measured in Chromium 153, ten copies of a zero-width character leave the bubble at 24 px wide — the padding and no more — while ten copies of U+3164 take it to 184 px. The recipient gets a message with no text in it and a bubble whose width counts how many characters you sent.

Ten copies ofBubble widthCounter in the field
U+200B24 px10
U+206024 px10
U+FEFF24 px10
U+3164184 px10
U+E002024 px20

The last row is a second way the numbers on screen stop matching the message. U+E0020 is a tag character, and one of them is two UTF-16 units, so a field that counts units reports twenty where you typed ten — and neither number is the length of the message the recipient sees, which is zero either way. The width also shows up in the notification: a chat list row is drawn from the same string, so a blank message of U+3164 produces a gap after the sender's name where an ordinary message would have a word, and a message of U+200B produces a row that looks exactly like a message with no text.

Send blank message: what the app says when it refuses

The wording of the refusal names the check. One messaging API returns Cannot send an empty message to developers who assemble the payload wrong. A chat app returns Can't send empty message when an Android intent arrives with an empty body. A bot platform answers with the same phrase again, and it appears across three separate questions from three different people who were each right that their own code was fine — because the payload the app built was genuinely empty even though the field they typed into was not.

That is the useful part: the check runs on the payload, and it is a string check, so it accepts anything a trim leaves behind. It also means the failure can come from a step between your field and the payload — a share link that collapses its line breaks, a text extraction that returns nothing, a reply window that opens with no body. It is also why “try a space or an HTML entity” is a dead end, and you can watch someone walk into it: on a bot platform where the send method requires text, the asker reports trying a space and trying an entity and getting nowhere.

The field type changes none of this. Measured across an <input>, a <textarea> and a contenteditable, all three give the same trim verdict for all 23 characters — the same 16 pass and the same seven fail. A comment box that is not an input is not testing something different; it is testing the same thing on a string it has to go and collect.

Blank message for Instagram: the same rule with one extra hop

The rule does not change on a social app, but the box might, and that adds a step that can lose the character. A comment or direct-message box on the web is usually a contenteditable element rather than an input, and a contenteditable is not submitted by a form at all. Measured with FormData, a form holding an input, a textarea and a contenteditable submits two keys, and the contenteditable contributes nothing. Its text has to be read out and written into a hidden field first.

That hop is where a blank message fails for a reason that has nothing to do with the character. The value has to survive being read as innerText or textContent, written into a hidden input, and then encoded for the request. Measured on all 23, innerText and textContent both keep the character and report a length of one, and the trim verdict at the end of it is the same 16 of 23. What is not safe is any step that normalises or strips the string on the way, and there is no way to see from the outside whether one is in the path.

One CSS detail catches people building this themselves. The usual way to fake a placeholder in a contenteditable is a rule on :empty, and :empty tests for text nodes rather than whitespace. Measured in Chromium 153, a div holding the empty string matches :empty, one holding a single ordinary space does not, and one holding U+200B does not either. The fake placeholder vanishes on a box that will refuse to send the thing that made it vanish — the same false signal as the real placeholder. The limit here is the app, not the rule: nothing in this section was measured against Instagram, and a platform can run the trim check and then four more on top of it.

Where the advice splits

Everyone agrees on the first move: a space will not do it, and an invisible character will. The disagreement starts with which character. The common advice is a Hangul filler, and the reasoning is sound as far as it goes — it draws nothing, it survives a trim, and it is a letter, so a field that accepts letters accepts it. What the advice leaves out is the width. Measured, U+3164 advances a full em, and that is the one property of the character a reader can see. The position that follows is the opposite of the common advice: for a blank message you want no advance at all, and the fillers are the wrong end of the list.

The second position is that the exercise is about the wrong layer, and that the interesting question is what the receiving end does with the string. On that reading a blank message is not a trick that works or fails but a probe that identifies which check a platform runs. Refusing it means the payload is trimmed. Accepting it means the payload is checked for emptiness alone. Accepting it and then rendering a gap means nothing is normalised and advance widths are being measured.

The third position is the minority one, and it cuts against both of the others: the character is a liability in your own data, not a harmless trick in someone else's chat. The evidence is not about messages. A configuration value with a leading zero-width space produced an error that named the code point and nothing else, and the person who found it reported being surprised that their language's own strip() did not remove it — the same seven-of-23 split measured above, arriving as a production bug instead of a curiosity. Elsewhere the same character got past an input sanitiser and threw an error naming U+200B. A character that is whitespace to nobody is not stripped by anything that strips whitespace, and that cuts both ways: it is why the message sends, and it is why the same character survives every cleanup step you have.

What this page does not tell you

Whether any particular app accepts a blank message today. What is measured here is the box, not the service: the renderer's treatment of a whitespace-only string, the trim-based emptiness check most send buttons implement, the ink and advance width of 23 characters, and three kinds of form field in one browser. Every service layers its own rules on top and changes them without notice.

The set of 23 is the set measured, not a canonical list of every invisible character, and a different set moves the counts without changing the direction. The ink and advance figures are specific to one font stack on one platform, and a device with different font coverage can substitute a glyph — which is where the box comes from, and nothing measured here predicts which devices do that. Chromium 153.0.8010.12 is a single build, and the trim list is defined by the Unicode version that build carries.