Formatting Hebrew Text Correctly in a Chat Interface
10 min read · updated August 11, 2026
A chat interface is the hardest RTL surface to get right, because it mixes user text, model text, code, links and interface chrome in one column, and any one of them can be in a different direction from the others. The good news is that there are about six settings, and once they are in place the layout stops fighting you.
Direction is per message, not per app
The instinct is to put dir="rtl" on the document when the user’s locale is Hebrew. That is wrong for a chat product for a specific reason: a Hebrew-speaking user routinely pastes an English error message, a stack trace or a URL, and asks about it in Hebrew. Two consecutive messages in the same thread have opposite directions. Setting direction on the document forces every neutral in the English message to resolve right-to-left, so the punctuation in the pasted stack trace scatters.
Direction belongs on the smallest element that contains one coherent run of text — the message bubble, and inside a long message, the paragraph. Everything above that level is layout, and layout should be expressed in logical properties so that it mirrors when the direction does without any duplicate rules.
/* the thread container: layout only, no text direction */
.thread { display: flex; flex-direction: column; }
/* the bubble: direction is set per message from a data attribute
or dir=auto; alignment follows direction automatically */
.bubble {
text-align: start;
padding-inline-start: 14px;
padding-inline-end: 10px;
border-inline-start: 2px solid var(--accent);
margin-inline-end: auto;
}
/* multi-paragraph message: resolve each paragraph separately */
.bubble p { unicode-bidi: plaintext; }text-align: start instead of left, and the -inline-start / -inline-end properties instead of left and right, are the whole of layout mirroring. Every rule written with a physical side is a rule you will have to duplicate under an [dir="rtl"] selector, and the one you forget is the bug.
What dir=auto actually does
dir="auto" in HTML applies a first-strong heuristic, defined in the HTML Living Standard: scan the text for the first character with a strong direction — Latin, Hebrew, Arabic — and take the paragraph direction from it, ignoring numbers, punctuation and whitespace entirely. It is the same heuristic as the Unicode FSI isolate.
It is right most of the time and its failures are predictable. A Hebrew message that opens with an English brand name, a code identifier, a bullet character or a Markdown heading marker followed by an English word is detected as left-to-right, and the entire message is laid out wrongly. A message that is a bare URL, or a number, has no strong character at all and falls back to the inherited direction.
The robust arrangement is to use the language you know rather than guessing from the glyphs: you asked the model for Hebrew, so set dir="rtl" and lang="he" on that message. Keep dir="auto" for content whose language you genuinely do not know — user input, pasted text, a tool result. And use unicode-bidi: plaintext on paragraphs inside a long message, which applies the same first-strong rule per paragraph, so a mixed-language answer resolves each block on its own merits.
The streaming flip
Here is the failure unique to AI chat. Responses stream token by token, and the DOM is updated on every chunk. With dir="auto" on the bubble, the direction is recomputed as the text changes — so a response whose first chunk is "OK" or a code fence renders left-to-right, and then flips to right-to-left the moment the first Hebrew letter arrives. The user watches the message jump across the screen, and any text already rendered re-lays-out under it.
It is not a rendering bug and there is nothing to fix in the CSS. The heuristic is being asked a question about a string that keeps changing. Three ways out, in order of preference:
- Decide from the request. You chose the response language, or you detected the user’s. Set the direction on the bubble before the first chunk arrives and never change it.
- Decide once, late. If you must detect, buffer until the first strong character appears, then fix the direction for the life of the message. One decision, not one per chunk.
- Ask the model. Where the response language is genuinely open, have the model return the language tag as a structured field alongside the text, and set direction from the tag.
English, code and links inside a Hebrew message
Inside a right-to-left bubble, anything left-to-right needs isolating so it does not drag the neighbouring punctuation with it. The HTML element for this is <bdi>, which is defined as unicode-bidi: isolate plus dir="auto". Use it for inline code, product names, identifiers, file paths and usernames.
<div class="bubble" dir="rtl" lang="he">
<p>… <bdi>useEffect</bdi> …</p>
<!-- code blocks are always LTR, whatever the message is -->
<pre dir="ltr" style="unicode-bidi: isolate; text-align: left">
<code>const x = 1;</code>
</pre>
<!-- links: isolate, and let the URL break anywhere -->
<a dir="ltr" style="unicode-bidi: isolate; overflow-wrap: anywhere"
href="https://example.com/a/very/long/path">…</a>
</div>Code blocks are the case people miss. Source code is left-to-right regardless of the surrounding language: an unisolated code block in an RTL paragraph moves its semicolons, brackets and comment markers to the wrong side and produces code that will not run when copied. Set dir="ltr" and text-align: left on every <pre>, unconditionally. Long URLs have their own complications, covered in why RTL layout breaks on long URLs.
Everything that is not the message text
The message bubbles are usually the part that gets fixed and the surrounding interface the part that stays broken. Check each of these against a Hebrew thread:
- The composer. Set
dir="auto"on thetextareaso the caret starts on the correct side and moves correctly, and mirror the send button with a logical property. A placeholder in Hebrew inside an LTR input aligns left and looks broken before a single character is typed. - Timestamps and read receipts. Positioned with
inset-inline-end, notright. The time itself is digits and stays left-to-right, which is correct. - Icons that imply direction. A reply arrow, a back chevron, a send arrow. These should mirror; a clock, a checkmark or a logo should not. There is no automatic rule —
transform: scaleX(-1)under a direction selector, chosen icon by icon. - Truncation and ellipsis. An ellipsis is a neutral, so a truncated Hebrew string can put its ellipsis on the wrong end unless the truncating element carries the direction.
- Markdown rendering. Lists, tables and blockquotes have their own failures inside an RTL container — see why Markdown breaks in RTL output.