Mixed-Direction Text: Handling Arabic and English in the Same UI Element
A deep dive into the Unicode Bidirectional Algorithm's embedding levels, the real differences between dir="auto", <bdi>, <bdo> and Unicode's bidi control characters, and how to apply them to search boxes, chat bubbles, data tables and form labels.
A username in Latin script sitting inside an Arabic sentence. A price in Western digits at the end of an RTL product description. An English brand name typed into an Arabic search box. None of these are edge cases in a MENA product — they are most of the content, because real usage mixes scripts constantly and the browser has to decide, character by character, which way each piece should run. That decision is the output of a specific, documented algorithm, and understanding what it actually does is what separates "I wrapped it in <bdi> and it looked fine" from knowing why it worked, when it won't, and which of HTML's several overlapping tools is the right one for a given string.
Our companion piece on building a design system for LTR and RTL already covers why <bdi> belongs in a base text primitive rather than in every feature team's markup, and our guide to testing for RTL bugs covers hunting for the bugs that show up when isolation is missing. This post doesn't repeat either — it goes one layer deeper: the Unicode Bidirectional Algorithm mechanics that make mixed-direction text reorder the way it does, the real differences between dir="auto", <bdi>, <bdo> and CSS unicode-bidi, the Unicode control characters (LRM, RLM, LRE, RLE, PDF, LRI, RLI, FSI, PDI) and which the standard now discourages, and markup for the UI patterns that actually need this: search results, chat messages, data tables and bilingual form labels.
Key Takeaways
- Mixed-direction bugs aren't random. They come from the Unicode Bidirectional Algorithm (UAX #9) resolving "neutral" characters — digits, punctuation, spaces — against whatever strong-direction text surrounds them, exactly as the algorithm is designed to.
<bdi>isolates a run of text so it doesn't affect, or get affected by, its neighbors;<bdo>does the opposite — it forcibly overrides the rendering order of the characters inside it. They solve different problems and are not interchangeable.dir="auto"and the UnicodeFSI(First Strong Isolate) character do the same first-strong-character detection — one as an HTML attribute, one as an invisible character for contexts where you can't add markup.- Unicode explicitly discourages using the embedding characters LRE, RLE and PDF in new documents, in favor of the isolate characters LRI, RLI, FSI and PDI, because embeddings let inner text "leak" directional influence onto whatever follows them, while isolates don't.
- CSS's
unicode-bidiproperty is the low-level mechanism<bdi>anddirare built on, but MDN's own documentation warns it "is intended for Document Type Definition (DTD) designers" and that web authors generally shouldn't reach for it directly — use the HTML-level tools first.- Reach for an invisible Unicode mark or isolate character only when you have no markup to attach to — plain-text notification payloads, SMS, CSV exports, log lines. Inside HTML, W3C's guidance is to use markup for the same effect wherever markup is available.
What the Unicode Bidirectional Algorithm is actually resolving
Every character in Unicode carries a bidirectional type: strong (definitely left-to-right, like Latin letters, or definitely right-to-left, like Arabic and Hebrew letters), weak (numbers, currency symbols), or neutral (spaces, most punctuation). The Unicode Bidirectional Algorithm, specified in UAX #9, exists because a line of text isn't reliably one direction once it mixes those types, and without a consistent algorithm, different renderers would order the same mixed-script text differently.
The mechanism it uses is the embedding level: a number tracking how deeply nested the current run of text is, where even levels resolve left-to-right and odd levels resolve right-to-left. A paragraph starts at level 0 (LTR) or 1 (RTL) depending on its base direction, and explicit direction changes — via markup or Unicode control characters — push the level up or down from there, with the standard capping nesting at 125 levels so no document can produce runaway complexity for a renderer.
The part that actually produces visible bugs is how neutral and weak characters get assigned a level. W3C's explainer on the algorithm's basics puts the core rule plainly: "a neutral character between two strongly typed characters that have the same directional type will also assume that directionality," and when the surrounding characters disagree, the neutral falls back to the paragraph's base direction. That single rule is why an RTL name followed by " - 1st place" can render with the punctuation and digit pulled out of place without isolation: the hyphen, space and digit are neutral or weak, so the algorithm resolves them against whichever strong direction is nearest, dragging the name's characters along with them. Nothing is malfunctioning — the algorithm is doing exactly what UAX #9 says it should. The bug is that nobody told it where the RTL run actually starts and ends.
Four tools, four different jobs
HTML and CSS give you four ways to intervene in that resolution, and they are not interchangeable — reaching for the wrong one does nothing, or breaks the text you're trying to fix.
dir="auto" tells the browser to pick a base direction for an element by scanning its own text for the first strongly-directional character, skipping over <bdi>, <script>, <style>, <textarea> and any nested element with its own valid dir. MDN is explicit this is a heuristic, not language detection: "a comment that starts with an English name and continues in Arabic will get a left-to-right base direction." It's right for content whose direction genuinely isn't known ahead of time — a search box, a comment field, a chat input — and wrong for content whose direction you already know, where explicit dir="rtl" or dir="ltr" is cheaper and more reliable.
<bdi> isolates a span of text from its surroundings in both directions: per MDN's documentation, text inside <bdi> doesn't influence the directionality of what's around it, and isn't influenced by it either. Internally it behaves like dir="auto" scoped to just that span — the fix for the earlier "1st place" bug, since it gives the name its own embedding level that surrounding neutrals can't drag it out of.
<bdo> does close to the opposite. It doesn't isolate anything — it forces the characters inside it to render in a specific order regardless of their own strong type, per MDN's description: the dir attribute is required, and "the individual characters' orientation is not affected... so characters don't get drawn backward." <bdo> shows up rarely in product UI — its legitimate uses are things like deliberately displaying a fragment in the opposite of its natural order, not everyday bilingual content. If you're reaching for <bdo> to fix a garbled username or price, you almost certainly want <bdi> instead.
CSS unicode-bidi is the property both of the above are effectively shorthand for at the browser level, with values including isolate (the mechanism behind <bdi>), bidi-override (the mechanism behind <bdo>), and plaintext (resolve direction purely from the Unicode algorithm, ignoring the parent's direction entirely). MDN's own guidance is unusually direct about not using it casually: the property "is intended for Document Type Definition (DTD) designers," and ordinary authors "should not override it." In practice: use <bdi>, <bdo> and dir first, and treat unicode-bidi: isolate as a fallback for a design-system primitive with no natural element to attach to — a <span> standing in for a value with no semantic wrapper — not something to sprinkle through feature code.
The Unicode control characters, and why half of them are discouraged
Sometimes there's no element to attach dir or <bdi> to at all — inside a plain-text SMS, a push-notification payload, a CSV export, a log line. Unicode defines invisible, zero-width characters for this, and its guidance on which ones to use has genuinely changed over time.
LRM and RLM — the Left-to-Right Mark (U+200E) and Right-to-Left Mark (U+200F) — are invisible characters that behave like an ordinary strong-direction letter of their respective direction, without displaying anything. They don't open or close a scope; they just give the algorithm one more strong character to resolve neighboring neutrals against — useful for nudging a single stray neutral character, a colon or parenthesis landing on the wrong side of a number or script boundary, without wrapping anything in a container. Unicode also defines the Arabic Letter Mark (ALM, U+061C), which UAX #9 describes as providing the same directional effect as RLM but specifically in Arabic-script contexts — use RLM for Hebrew or other RTL scripts, ALM when the surrounding strong-RTL text is specifically Arabic.
LRE, RLE and PDF — Left-to-Right Embedding, Right-to-Left Embedding, and Pop Directional Formatting — are the older mechanism for opening and closing a directional scope in plain text: LRE and RLE push the embedding level up in their direction, and PDF pops back to the previous level. Unicode now explicitly discourages using them in new documents, because an embedding still lets its contents interact with the surrounding reordering — the standard's own language is that embeddings "usually have too strong an effect on their surroundings and are... unnecessarily difficult to use." That's the plain-text version of the same bug isolation prevents: an embedded run can still contaminate what follows once the algorithm reorders neutrals across the boundary.
LRI, RLI, FSI and PDI — Left-to-Right Isolate, Right-to-Left Isolate, First Strong Isolate, and Pop Directional Isolate — are what Unicode recommends in their place. LRI and RLI open an isolated run forced to a specific direction; FSI opens one auto-detected from its own first strong character, the plain-text equivalent of dir="auto"; PDI closes whichever of the three most recently opened. Unlike LRE/RLE, none let their contents affect ordering outside them — exactly why W3C's guidance on Unicode bidi controls recommends "the Unicode Standard to use RLI and LRI, and avoid using RLE and LRE." The same source is clear that inside HTML, markup should generally do this job instead.
Pattern: a search box mixing English and Arabic results
A search input itself should use dir="auto" so the query field's own alignment follows whatever the user is actually typing, rather than inheriting the page's direction:
<input type="search" dir="auto" lang="ar" placeholder="ابحث عن شركة أو منتج..." />
The results list mixes scripts on every row — a company name that might be Latin or Arabic, a category label in the interface language, a price in Latin digits. Isolate the parts whose script you don't control, and leave the parts you do control alone:
<li class="result-row">
<bdi class="result-name">Careem</bdi>
<span class="result-category">النقل والتوصيل</span>
<bdi class="result-price">129.50 د.إ</bdi>
</li>
Wrapping result-name and result-price in <bdi> means a Latin brand name can't drag the Arabic category label's punctuation out of place, and an Arabic business name can't do the same to a neighboring Latin price — regardless of which combination shows up in a given row.
Pattern: chat bubbles with mixed-direction messages
Chat is the clearest case for dir="auto" on a per-message basis, because you cannot know a message's language until it's typed, and a single conversation legitimately switches sentence to sentence:
<div class="message-bubble" dir="auto">
<p class="message-text">Wait, هل يمكنك إرسال الملف الآن؟</p>
<span class="message-time">14:32</span>
</div>
Two details matter here. First, dir="auto" should sit on the text itself, not on the bubble's outer container if that container also positions the avatar and sent/received alignment — otherwise a message that starts with an Arabic word can flip which side of the screen the whole bubble sits on, a layout decision that should stay tied to who sent the message, not what they typed. Second, the timestamp is a case for <bdi> rather than dir="auto": a short, known-format string that should never inherit a bidi decision made for the message body beside it.
.message-bubble {
/* Layout side is a sender property, not a text-direction property */
margin-inline-start: var(--sent-by-me, auto);
}
.message-text {
unicode-bidi: isolate; /* paired with dir="auto" above */
}
Pattern: data tables with mixed-language cells
Tables surface a different failure mode: a column can be internally consistent (all names, all prices) while the table as a whole mixes scripts across rows, and a dir set once on the whole <table> is wrong for whichever column doesn't match it.
<table dir="rtl" lang="ar">
<tr>
<th>الشركة</th>
<th>الحالة</th>
<th>الإيراد الشهري</th>
</tr>
<tr>
<td><bdi>Talabat</bdi></td>
<td>نشط</td>
<td dir="ltr">$42,300</td>
</tr>
</table>
The company-name cell gets <bdi> because its content is unpredictable per row. The revenue cell gets an explicit dir="ltr" rather than <bdi> or dir="auto", because currency figures always render left-to-right regardless of what's next to them — you already know the direction, so explicit dir is cheaper and more predictable than auto-detection. Sort indicators and column headers should follow the table's own dir, not the cell content's, since they're interface chrome rather than data.
Pattern: form labels with mixed content
Bilingual labels — "الاسم الكامل (Full name)", a label followed by a unit like "المدة (بالأيام)" — break often because the label is typically one translated string with a fragment spliced in by a template, not hand-written per instance:
<label for="duration" dir="auto">
مدة الحملة <bdi>(days)</bdi>
</label>
<input id="duration" type="number" dir="ltr" />
The parenthetical gets <bdi> for the same reason a username does: its content — a fragment that might be substituted or translated independently of the label — shouldn't pull the parenthesis or the space before it out of place. The input itself is forced dir="ltr" outright, following the same logic as email and phone fields in our RTL testing guide: numeric entry is Latin-script data even inside an Arabic form, so it never benefits from auto-detection.
Common mistakes
Using <bdo> where <bdi> was needed. <bdo> forces a rendering order; it doesn't protect a span from its neighbors. Reaching for it to fix a garbled username makes the garbling worse, because it overrides characters that were already correct.
Setting dir="auto" on content whose direction you already know. Auto-detection is for genuinely unknown input. A price field or a UI label with a fixed translation should get an explicit dir, both for predictability and because auto-detection isn't free.
Reaching for LRE/RLE/PDF in new code. They still work, but the standard steers new documents toward LRI/RLI/FSI/PDI specifically because embeddings can leak direction onto neighboring text in ways isolates can't.
Sprinkling unicode-bidi through component CSS. It's the mechanism underneath dir and <bdi>, not a replacement for them — MDN frames it as a tool for markup-language authors, not feature code.
Isolating everything, including strings you already know are single-script. <bdi> and dir="auto" cost a directionality scan; applying them reflexively to every span, including ones with a template-guaranteed fixed language, solves a problem that string doesn't have.
Get the mechanics in this post right and the bug-hunting checklist in our RTL testing guide becomes mostly confirmation rather than discovery — you already isolated the values that needed it, forced direction on the ones that didn't, and left the Unicode control characters for the plain-text contexts that actually require them.