Bidi isolation in HTML: bdi, dir, and CSS
Learn why inserted text can rearrange an RTL sentence, when to isolate it, and how to choose between bdi, dir, and CSS.
By Omran Khleifat, Founder of Ritla
Guide12 min read
A cold-storage dashboard shows the sensor ID 436-07-8921 correctly in its detail panel. Put the same ID after an Arabic label in an alert, and its groups appear as 8921-07-436 when read from left to right. The database value has not changed. The browser has interpreted the hyphens and digit groups in the context of the Arabic sentence. This is a poor place to discover that a sensor ID is not a sentence.
Bidi isolation gives the inserted value a boundary. Inside that boundary, the browser resolves the value's direction without borrowing directional context from the sentence. Outside it, the sentence treats the value as a single unit, rather than letting its characters influence neighboring punctuation or numbers. That is the useful mental model. It is not an instruction to reverse text, mirror the component, or ask CSS to move the alert to the other side of the screen.
For HTML, prefer a tightly wrapped element with dir: use a known direction such as ltr for a fixed-order identifier, or auto when the inserted value may be Arabic or Latin. The <bdi> element is a convenient isolated wrapper whose default direction is automatic. CSS unicode-bidi: isolate can provide the rendering behavior when markup is not under your control, but it is a less suitable way to express the meaning of authored HTML. The choices are similar on a screenshot and different in the source, which is where tomorrow's bug begins.
What isolation actually separates#
The Unicode Bidirectional Algorithm works from stored character order, character types, and paragraph direction to determine visual order. Arabic letters are strongly right-to-left; Latin letters are strongly left-to-right; digits and many punctuation marks depend on context. An unmarked inline value is part of its containing paragraph's bidi calculation. A plain <span> can change its color without creating a new directional boundary. The browser does not infer that a CSS class named sensor-id contains a sensor ID. It has no interest in your naming conventions, however admirable they may be.
Isolation changes the scope of that calculation. The characters inside an isolate are resolved together. For the surrounding sentence, the isolate behaves roughly like one neutral object. This prevents the inserted content from changing the ordering of nearby text and prevents nearby text from deciding the value's internal order. The W3C guidance on inline bidi markup explains why a tightly wrapped directional phrase is more reliable than hoping its neighbors will be friendly.
Isolation does not make every value LTR. Its internal base direction still matters. An isolated Arabic name should be read as Arabic; an isolated fixed-order Latin code should keep its LTR order. Nor does isolation change the DOM string. Selection and copying still operate on the logical characters, so copying a value that looks wrong is not a visual test. You may copy the correct ID from a display that an operator cannot read correctly. Both facts can be true, which is inconvenient but not a browser contradiction.
It also helps to distinguish isolation from embedding. An embedding gives text a directional level but still permits interactions across its boundary. An isolate is deliberately opaque to the surrounding bidi calculation. In CSS, unicode-bidi: embed and unicode-bidi: isolate are therefore not alternate spellings of the same fix. MDN's unicode-bidi reference describes the different effects.
A fixed-order ID inside an Arabic alert#
The alert's Arabic label and the sensor ID are both valid content. The problem is that the identifier has a fixed group order, while the unwrapped hyphens have no idea they belong to an identifier.
Problem
<p class="sensor-alert" lang="ar" dir="rtl"> تعذر الاتصال بالمجس 436-07-8921 في الثلاجة.</p>In the Chromium 151 behavior checked for this guide, Western-digit groups separated by hyphens after Arabic letters appear in reverse group order: 8921-07-436 from left to right. The digits inside each group remain in their own order. A bare RTL cell containing the same hyphenated value can display as 436-07-8921, because it lacks the preceding Arabic letters. If you test only the detail panel, you miss the alert. The browser-rendering guide follows that contextual change through the algorithm; here the question is where to put the boundary.
Better
<p class="sensor-alert" lang="ar" dir="rtl"> تعذر الاتصال بالمجس <bdi dir="ltr">436-07-8921</bdi> في الثلاجة.</p>The fixed-order ID is now isolated and explicitly LTR. Its Arabic sentence remains RTL. dir="ltr" is not a request to make the page LTR. It applies to the element that contains only the value. In modern HTML, a tightly wrapped <span dir="ltr">436-07-8921</span> has the same directional effect; use a semantic element already present rather than adding wrappers for sport. The important parts are the boundary and the value's known direction.
Why use an explicit direction if <bdi> already isolates? Because this is an identifier whose reading order is part of its meaning. <bdi> without dir defaults to automatic direction. In the checked browser, this digit-only value resolves to LTR under auto, so it also displays as intended. Explicit ltr records the requirement in the markup and remains clear if the product later puts a letter or symbol at the start. It tells the next developer what the field is, not merely what the current sample happens to do.
Do not apply this rule to every hyphenated number in an Arabic sentence. A range can be read correctly from right to left even when its groups look reversed to someone scanning left to right. For the sensor ID, the group sequence must match a label on the physical device and a search key in support. That product requirement, not the shape of the hyphen, makes LTR isolation appropriate. If a field's meaning is ambiguous, ask what an Arabic reader should read first before changing its direction.
Choose the wrapper by what you know#
The markup choice is less mysterious when you separate known direction from unknown direction.
| Situation | Markup to start with | Why |
|---|---|---|
| Fixed-order LTR value in Arabic prose | <span dir="ltr">436-07-8921</span> | The value has a known reading order and the inline dir isolates it. |
| Same value with no existing wrapper | <bdi dir="ltr">436-07-8921</bdi> | Adds a small boundary around the value. |
| User or partner text that may begin in either script | <bdi>text</bdi> or <span dir="auto">text</span> | Direction comes from the value's first strong character. |
| Existing semantic inline element | <cite dir="auto">text</cite> | Keep the element's meaning and add direction to it. |
The table is not a mandate to use <bdi> for every Latin word. Many Latin runs read correctly inside RTL paragraphs without any markup. Isolate content at the insertion boundary when it must retain its own direction or must not disturb the sentence around it: user names, vendor names with punctuation, identifiers, URLs, email addresses, and other values whose origin is separate from the translated sentence.
Consider a vendor label on the same cold-storage dashboard. Its registered name is PolarHub+, including the plus sign. Insert it between Arabic words without a boundary, and the checked browser places the plus sign before the Latin letters, yielding +PolarHub from left to right. That is a different failure from the sensor ID, but the boundary fixes it for the same reason.
Problem
<p lang="ar" dir="rtl">وصل تنبيه من <span>PolarHub+</span> للمشرف.</p>Better
<p lang="ar" dir="rtl">وصل تنبيه من <bdi>PolarHub+</bdi> للمشرف.</p>Here <bdi> uses the first strong Latin letter to choose LTR, and the plus sign remains inside the vendor name. The plain <span> in the first version does not isolate anything. If the dashboard's partner directory can contain Arabic names as well, automatic direction lets each name supply its own base direction. This is exactly the case <bdi> was made for, as MDN's element reference describes.
There is a limit to auto: it reads characters, not intent. If the value has no strongly directional letter, the checked browser resolves it to LTR. That happens to suit our Western-digit sensor ID, but it is not a general product rule for all numbers. A numeric range may need RTL reading order, and spaced Arabic-Indic digit groups can behave differently even inside an LTR isolate. When the field's direction is known from its data contract, state it. When the content is genuinely unknown, auto is useful. Do not use it as a substitute for deciding what the field means.
Wrap the value, not the whole sentence#
An isolate should contain the independent value and its own punctuation, but not the translated words or punctuation that belong to the surrounding sentence. If you wrap the Arabic label and the ID together under dir="auto", the first strong character is Arabic. The value has not gained its own LTR context. You have isolated the entire alert from its parent, which may be a fine thing for the alert but does not solve this bug.
<!-- The ID is still part of one Arabic-direction run. --><p lang="ar" dir="rtl"><bdi>المجس 436-07-8921</bdi> لا يستجيب.</p> <!-- The ID has its own boundary. --><p lang="ar" dir="rtl">المجس <bdi dir="ltr">436-07-8921</bdi> لا يستجيب.</p>Likewise, leave sentence punctuation outside the isolate. If the alert ends with a period, the period belongs to the Arabic sentence. If the plus sign is part of PolarHub+, it belongs inside the vendor-name isolate. This ownership test is more useful than a blanket rule about punctuation: would the mark travel with the value if the value moved to a different sentence? If yes, include it. If not, leave it with the sentence.
Template boundaries matter too. A translation should own its words and punctuation while the inserted value remains a separate node. Do not build a string and then try to repair its display by reversing substrings. The DOM's logical order should remain the order the text was written. A bidi boundary controls presentation without corrupting the identifier that search, logs, and copy operations use. For a broader treatment of mixed-language templates, see Arabic and English mixed text in HTML.
Wrapping is not a substitute for choosing the paragraph direction. A page with lang="ar" but no dir="rtl" still has an LTR base in the checked browser. An isolated sensor ID inside the wrong-base page can look right while the surrounding Arabic punctuation, alignment, and component flow remain wrong. Set the known page direction on the document, then isolate the exceptional inline values. The HTML dir guide covers that outer boundary.
What dir and CSS each contribute#
For authored HTML, the dir attribute communicates directional meaning. On a tightly wrapped inline element, it sets the element's base direction and provides isolation from neighboring text. <bdi> provides isolation by default and takes automatic direction unless you give it dir="ltr" or dir="rtl". These are HTML decisions about the text, not decorative choices. The HTML dir reference and W3C inline-markup guide explain the behavior.
When markup is controlled by another system, CSS can express the same visual isolation on a wrapper you can select:
<p class="sensor-alert" lang="ar" dir="rtl"> تعذر الاتصال بالمجس <span class="sensor-id">436-07-8921</span> في الثلاجة.</p>.sensor-id { direction: ltr; unicode-bidi: isolate;}Both declarations matter in this example. direction supplies the known internal base; unicode-bidi: isolate prevents the inline span's content from participating in the surrounding bidi calculation. CSS direction: ltr alone on an ordinary inline <span> is not a reliable replacement for an HTML dir boundary. MDN's direction reference warns that the CSS property alone has no effect on inline-level elements unless unicode-bidi establishes the appropriate context.
If you own the HTML, write <span dir="ltr"> or <bdi dir="ltr"> instead of hiding the text's direction in a stylesheet. The CSS may be absent in another rendering context, changed by a theme, or omitted when content is reused. The markup stays with the value. CSS remains a legitimate tool for a wrapper supplied by a third-party component or a rendering system you cannot change; just be honest that it is a presentation-layer repair and test that rendering path.
Avoid unicode-bidi: bidi-override for a normal identifier. An override tells the browser to disregard the characters' usual directional behavior within the element. It is not a stronger isolate; it solves a different and rarer problem. The same warning applies to <bdo>. If your ID needs to keep its group order, give it a base direction and isolation, not a blanket override. concerns a forced-LTR container or bidi override over Arabic content, which is a good reason to inspect broad overrides before adding another one.
Nor will text-align: right, a flex reversal, or a mirrored icon isolate text. Those operate on boxes or their placement. The bug here is the order of directional runs inside the line. A UI can be aligned perfectly and still show the wrong fixed-order ID. Such elegance is best kept out of a release.
When the output cannot contain markup#
HTML is the easiest place to create a visible, inspectable boundary. Some destinations only accept a plain string: a text export, a title attribute, or a message handed to another system that does not preserve markup. Unicode provides isolate controls for that case: LRI (U+2066) starts a known LTR isolate, RLI (U+2067) starts a known RTL isolate, FSI (U+2068) chooses from the first strong character, and PDI (U+2069) ends it. The W3C guide to Unicode bidi controls describes their use when markup is unavailable.
For a display string containing our known-LTR sensor ID, a JavaScript example is:
const sensorId = "436-07-8921";const LRI = "\u2066";const PDI = "\u2069";const alertText = `تعذر الاتصال بالمجس ${LRI}${sensorId}${PDI} في الثلاجة.`;That is a fallback for a text-only output, not a suggestion to store the invisible controls inside the canonical sensorId. Keep the record key plain and apply controls when composing a display string that needs them. Pair the opening control with PDI and test the destination that will render the string. Invisible characters are hard to notice in a code review and easy to carry into a copied search key. If the destination supports HTML, use a tagged boundary you can inspect in DevTools.
Some systems strip, escape, or normalize text before display. If the controls are lost, the isolation is lost with them. A test in your local HTML page does not prove the exported notification behaves the same way. Check the final output path, including whatever component or client actually draws the text. This is not glamorous testing. Neither is explaining why a warehouse operator searched for an ID that looked different in the alert.
Debug the boundary, not just the screenshot#
When a mixed-direction value looks wrong, first compare it with the stored string. If the source itself was assembled in the wrong order, isolation cannot repair the data. If the source is correct, inspect the nearest element that contains only the value. Select that element in DevTools and evaluate:
({ text: $0.textContent, dirAttribute: $0.getAttribute("dir"), computedDirection: getComputedStyle($0).direction, unicodeBidi: getComputedStyle($0).unicodeBidi})$0 is the currently selected element in browser DevTools. The result tells you whether you selected the value rather than the whole sentence, whether an explicit dir is present, and whether CSS supplies isolation. Computed direction alone does not tell you whether a plain span is isolated. That is why the unicodeBidi field matters. Conversely, a dir attribute can express direction even when a stylesheet later interferes; compare the markup with the computed result.
For the sensor ID, make a small test with the same value in three contexts: alone in an RTL element, after the real Arabic label, and after that label with a tight LTR isolate. The bare version may look fine; the inline version exposes the interaction; the isolated version should preserve the fixed group order. This is a stronger test than taking one screenshot of the finished alert and declaring victory.
Then vary the things that product content actually varies. Put the alert in its narrow mobile card and its wide detail view. Test a value with the same separators as production data. If vendor names are user-supplied, try one beginning with Latin letters, one beginning with Arabic letters, and one containing punctuation that belongs to the name. Test the LTR route too: a fix that changes only the Arabic screenshot but damages the English rendering is not much of a fix. The expectation should be written in terms of the field's meaning, not “looks like the English version.”
Visual inspection is necessary. Copying the ID verifies the logical string, not how it appeared. A screen reader may also follow the logical content rather than the exact visual order, so it cannot replace a visual check of punctuation and group order. On the other hand, do not use a screenshot alone to claim data corruption. The tests answer different questions; make each do its own job.
reports unisolated bidi hazards such as phone numbers, email addresses, and Latin tokens inside Arabic text. It is relevant to the unisolated vendor name here. Do not assume it certifies the numeric sensor ID, whose fixed group order still needs the three-context test. If punctuation lands at the wrong visual end of Arabic text, describes that separate symptom. A free Ritla scan can point you toward those mixed-text cases, but the final judgment remains semantic: isolate a value because it has its own reading direction and boundary, not because every hyphen is suspicious.
Once that boundary is explicit, the Arabic sentence can do its job and the inserted value can do its own. For a deeper account of why the browser reorders the unwrapped version, read how browsers render mixed-direction text. Then come back and check the alert itself. Sensors are allowed to fail; their IDs should at least be legible when they do.