CSS unicode-bidi explained
Learn what each unicode-bidi value changes, how isolation differs from overrides, and how to debug mixed-direction text without breaking Arabic.
By Omran Khleifat, Founder of Ritla
Guide14 min read
An Arabic maintenance console should read from right to left while an equipment marker such as PUMP keeps its Latin letter order. Adding unicode-bidi: bidi-override to make the console “more RTL” can instead display that marker as PMUP. The property is doing what the declaration requested: overriding the characters' normal directional behavior.
unicode-bidi controls how text participates in bidirectional processing. It can leave inline boundaries transparent, establish an embedding, isolate a value, force directional ordering, or let individual text paragraphs determine their own direction. Those operations solve different problems. Trying values until a single screenshot looks right is a poor way to choose between them.
For HTML, prefer direction markup where it is available. Use this property to understand computed behavior, investigate CSS that interferes with markup, and handle a narrowly defined rendering requirement when CSS is the available control. W3C explains why direction belongs with the content in CSS vs. markup for bidi support.
Start by removing the accidental override#
Consider a maintenance dashboard showing a short equipment marker beside an Arabic label. The technician expects the Arabic label to read naturally and the marker to retain its documented spelling.
Problem
<p class="equipment-line" dir="rtl">المعدة: PUMP</p>.equipment-line { unicode-bidi: bidi-override;}The RTL override forces ordering of the paragraph's immediate inline content instead of allowing the Latin marker its normal LTR run. For this simple marker, the letters display as PMUP. That behavior follows the override definition in the CSS Writing Modes specification.
Better
<p class="equipment-line" dir="rtl"> المعدة: <span dir="ltr">PUMP</span></p>Remove the .equipment-line override rule. The paragraph's dir="rtl" establishes the Arabic context; the marker has its own explicit direction boundary. Do not leave the override on the ancestor and assume that adding a child wrapper is an adequate repair.
The bare Latin word would already keep its letters in order under normal bidi processing. Its wrapper expresses the boundary for an inserted equipment value; it is not a workaround for the browser reversing ordinary Latin words in RTL text. That distinction prevents a misleading diagnosis.
Test the paragraph with the old rule disabled, then removed from the stylesheet. Inspect any print stylesheet or compact-view rule that can reintroduce it. A fix in the default view does not establish that every rendering path uses the repaired declaration.
Separate direction, bidi participation and alignment#
Three controls can produce visibly different text, but they answer different questions:
directionsupplies a base direction for the scopes where it applies.unicode-bididetermines how a box participates in bidi processing and whether special scopes or overrides are introduced.text-alignpositions inline content within the available line space.
Right-aligning a broken identifier does not restore its group order. Changing direction on a normal inline box does not, by itself, establish a bidi boundary. The relationship between the first two properties is described in MDN's direction reference.
Do not infer a complete diagnosis from direction: rtl in DevTools. An RTL paragraph containing normal Latin text is expected. An RTL override containing that same Latin text is a different instruction. You need the bidi mode and the element's scope to explain the result.
The property also leaves the underlying string alone. A marker rendered backwards can still be returned correctly by textContent. Reversing the stored value to compensate couples the data to one defective presentation and gives other consumers the wrong string.
For the full rendering algorithm, use how the Unicode Bidirectional Algorithm works. Here, the practical concern is which boundaries and overrides your CSS introduces into that algorithm.
What each unicode-bidi value means#
The initial value is normal, and the property is not inherited. Its six specific values are summarized below. MDN's unicode-bidi reference provides the syntax and property details.
| Value | Main effect | Engineering judgment |
|---|---|---|
normal | Adds no special bidi scope for an ordinary inline box | Suitable for text that should participate normally; not an isolation fix |
embed | Introduces an inline directional embedding with normal internal processing | Does not provide the separation an inserted external value needs |
isolate | Processes inline content separately from surrounding text | Useful low-level isolation behavior; prefer HTML markup in HTML |
bidi-override | Forces the relevant inline content into directional sequence | Use only when forced ordering is an explicit requirement |
isolate-override | Isolates externally and overrides internally | Still an override, despite the reassuring first half of its name |
plaintext | Determines direction independently for each bidi paragraph or isolated sequence | Useful for text-only content with unknown paragraph directions |
These descriptions are not interchangeable for every display type. In particular, embed and isolate establish their scopes on inline boxes; they do not give a block container the same new behavior merely because its computed value has that name. bidi-override also has defined behavior for a block container's immediate inline content.
Use the table to identify the operation, then inspect the actual box type. Applying the same declaration to an inline span, a paragraph and a wrapper around several paragraphs is not an equivalent experiment.
Normal and embed do not isolate inserted text#
normal means that an ordinary inline wrapper does not create a new bidi scope. Styling a value with a color or a class leaves its text participating in the surrounding paragraph.
In the maintenance console, a work packet is identified by two Western-digit groups. Its documented order is 975308-6421; this is an identifier, not a range whose endpoints should be read from the right.
Problem
<p dir="rtl">ملف الصيانة: <span class="packet-ref">975308-6421</span></p>.packet-ref { direction: ltr; unicode-bidi: normal;}The class computes to LTR, but the normal inline boundary does not establish the separate scope the developer intended. After the Arabic label, the groups display as 6421-975308.
This concrete reordering follows the supplied measurements in Chromium 151, last checked on 2026-09-24. Use the same fixture to check supported Firefox and Safari versions; those engines were not part of the supplied measurements.
Changing the mode to embed introduces an embedding, but embedding and isolation are different operations. An embedding still has directional influence on the surrounding paragraph. Even if it makes this one identifier look right, it does not express that externally supplied values should be directionally separate from their neighbors.
For a component whose requirement is independence, choose isolation. Test that requirement with surrounding punctuation and text, not only by looking at the value alone. A successful screenshot of an embedded value cannot establish that neighboring content is unaffected.
Isolate separates a value without overriding its characters#
If CSS is the only available control for the existing inline wrapper, the narrow repair for the work packet is:
.packet-ref { direction: ltr; unicode-bidi: isolate;}Use this with the same paragraph from the preceding example, replacing its .packet-ref rule. The reference now displays as 975308-6421. The base direction is known from the identifier's format, and isolation separates its bidi processing from the Arabic label.
isolate does not infer direction from the text. If the class only sets isolation, the element's direction can still come from its ancestor. A CSS-only repair therefore needs a deliberate direction policy as well as the boundary. Do not turn this class into a generic LTR wrapper for arbitrary human-authored content.
Where the HTML can be changed, express the same known-format requirement directly:
<p dir="rtl">ملف الصيانة: <span dir="ltr">975308-6421</span></p>For an inserted maintenance-plan title with unknown direction, use an automatic boundary instead:
<p dir="rtl">خطة العمل: <bdi>فحص المضخة الاحتياطية</bdi></p>An existing title link can take dir="auto" directly. There is no benefit in replacing useful link semantics with a generic element to obtain isolation. W3C's inline bidi markup guidance covers those choices.
Isolation is not a guarantee that every string displays in source order. In particular, the supplied checks show spaced Arabic-Indic digit groups reordering even inside an isolate. Keep tests for those formats separate from this Western-digit identifier, and establish the required reading order before selecting a stronger intervention.
Bidi overrides need an explicit content requirement#
An override suppresses normal directional treatment for the relevant content. With RTL, that can reverse the visible sequence of Latin letters. With LTR, it can force Arabic content into an order that is wrong for ordinary reading. The opening marker failure is a direct consequence, not a browser defect.
isolate-override does not soften that internal behavior. It adds protection for surrounding text while retaining forced ordering inside. Choosing it because “isolate sounds safe” can leave the value itself broken.
There are legitimate uses for forced order, such as a deliberately constructed visual-order sample or a tightly specified legacy representation. Those need a documented reason and fixtures containing exactly the allowed data. They do not justify an override on a translated dashboard, a user note or every identifier in an application.
When HTML must express an intentional override, the relevant element is <bdo> with an explicit direction, not <bdi>. MDN's bdo reference explains that role. The similarity of the names is an easy source of code-review mistakes.
Do not add an override to fix a value whose logical order is actually wrong. Check the API response or source string first. Conversely, do not rewrite a correct string because an override rendered it incorrectly. Preserve the distinction between a data error and a display instruction.
For regression coverage, include an Arabic plan title, the Latin equipment marker and the packet reference under the component's actual ancestors. A rule that rescues the numeric sample while damaging human text does not meet the component's requirement.
Plaintext gives each text paragraph its own base direction#
A maintenance event viewer may receive plain text containing Arabic technician notes and English equipment messages. If the renderer preserves this as one text-only block, plaintext is relevant: it uses the Unicode paragraph heuristic for each bidi paragraph instead of taking that paragraph's base direction from CSS direction.
This text-only fixture is intentionally different from a container holding several HTML paragraphs:
<pre class="event-excerpt" dir="rtl">تم عزل المضخة للفحص.Inspect impeller.</pre>.event-excerpt { white-space: pre-wrap; unicode-bidi: plaintext;}The preserved newline separates the text paragraphs in this example. Check that the Arabic note uses RTL paragraph direction and the English message uses LTR, with the English full stop at the sentence's end. This behavior follows the paragraph-direction rules in the Unicode Bidirectional Algorithm.
The value does not mean unformatted text, escaped HTML, monospace text or whitespace preservation. white-space: pre-wrap handles preservation and wrapping here; unicode-bidi: plaintext handles bidi paragraph direction. Removing one declaration tests a different concern from removing the other.
A soft wrap caused by a narrow viewport does not create a fresh direction decision for every visual line. Otherwise resizing the viewer could reinterpret successive pieces of the same sentence as independent paragraphs. Test actual paragraph separators separately from wrapping.
Do not choose this mode for a fixed-format packet reference merely because the value contains digits. Use explicit direction for a known format. plaintext answers the problem of unknown paragraph direction; it is not an instruction to render all text exactly as stored.
If the viewer can render structured HTML instead, put direction on the content's actual paragraph elements. For pre and textarea, HTML also defines special automatic-direction rendering. Consult the HTML bidi rendering rules before treating a CSS declaration and an HTML attribute as universal substitutes.
Plaintext does not change computed direction or layout policy#
After applying the event-excerpt rule, DevTools can still report direction: rtl. That is consistent with plaintext: the bidi paragraph decision does not rewrite the computed direction property.
This distinction is useful during debugging. Seeing rtl in the computed panel does not prove that an English paragraph inside the text-only viewer is being processed with an RTL paragraph base. Inspect the bidi mode and the visible punctuation as well.
It also limits what the declaration can fix. Do not expect a child flex row to change its item progression because a nearby event message begins in English. Do not expect logical padding, alignment policy or other direction-dependent layout calculations to acquire an independently computed direction for each text paragraph.
The event viewer needs two decisions: how to process each text paragraph, and how to align or position its surrounding UI. A copy control, timestamp or severity badge should follow the component's intended layout. Keep those elements outside the text-only excerpt rather than making their arrangement depend on the event message's first letter.
When reviewing a reported alignment defect, record the expected placement separately from the expected text order. A paragraph can have its punctuation correctly processed while still sitting at an edge the product did not intend. Replacing plaintext with an override would address neither requirement correctly.
Non-inheritance does not erase an ancestor's effect#
unicode-bidi is not inherited. Setting plaintext on a wrapper does not automatically set it on descendant block elements. This matters when an event renderer changes from one text node to structured markup:
<div class="event-excerpt" dir="rtl"> <p>تم عزل المضخة للفحص.</p> <p>Inspect impeller.</p></div>The previous .event-excerpt declaration no longer establishes an independent automatic direction for each child paragraph. The children have their own boxes and bidi paragraphs. If the direction is unknown, put dir="auto" on each paragraph instead of relying on an outer CSS mode to propagate.
The reverse mistake is assuming that a descendant computed as normal is immune to an ancestor's override. Non-inheritance describes property computation. It does not mean a child's text exists outside every bidi scope established around it. Inspect the ancestor chain before adding local rules.
Avoid a blanket unicode-bidi: inherit repair. It explicitly copies a parent's mode into descendants, potentially creating extra scopes or repeating overrides where none were intended. Match the boundary to the content unit instead.
This gives a practical debugging sequence: inspect the text-bearing box, inspect its ancestors, then identify which declaration established the scope affecting the text. A single computed value on an arbitrary nested span is insufficient evidence.
Reset rules can preserve bad modes or remove good boundaries#
Two reset assumptions cause different failures.
First, the all shorthand excludes direction and unicode-bidi. Adding all: unset to a component that already matches a bidi override rule does not clear that rule's bidi mode. MDN's all reference documents the exceptions.
For example, this reset does not remove the explicit override:
.legacy-event { direction: rtl; unicode-bidi: bidi-override; all: unset;}Remove or correct the offending direction declarations according to the content contract. Do not stack broader reset rules on top and assume the word “all” includes them.
Second, explicitly setting the property to its initial value can remove a useful boundary. A generic rule such as this is not a harmless normalization:
/* Problem: explicitly strips bidi scopes from matching inline elements. */bdi,span[dir] { unicode-bidi: normal;}Remove that rule when it contradicts the HTML's directional semantics. On a normal inline box, initial and unset also lead to the property's initial mode; neither means “restore the element's HTML isolation behavior.” Do not replace the rule with another global value by guesswork. Inspect the browser's rules and remove the author declaration that displaced them.
Keep the repair narrow. A numeric identifier that still looks correct under one reset is not proof that an Arabic title or neighboring punctuation retained its boundary. Run the component fixtures after changing a reset stylesheet, including views that use an existing semantic element with dir rather than bdi.
Keep formatting spans from becoming accidental bidi boundaries#
A log highlighter can split one equipment message into several spans for color. A search feature can split a packet reference to mark the matching digits. Those new nodes are presentation details, but a broad bidi selector can give them directional meaning the product never intended.
For example, this rule assigns a separate scope to every fragment inserted by a text highlighter:
/* Problem: formatting fragments are not necessarily independent values. */.maintenance-result span { direction: ltr; unicode-bidi: isolate;}The intact packet reference and the same reference split across highlighted fragments no longer have equivalent boundaries. Do not assume that each digit fragment, separator and surrounding word should be processed independently just because the renderer put it in a span.
Assign the boundary to the complete value instead:
<p class="maintenance-result" dir="rtl"> ملف الصيانة: <span dir="ltr">975308-<mark>6421</mark></span></p>Remove the broad span rule. The outer direction element owns the complete identifier; the inner mark indicates the search match. The expected visual identifier remains 975308-6421, with the final group highlighted. Verify that in the actual renderer, including any styles it applies to mark, rather than inferring correctness from this simplified markup alone.
This distinction also matters when migrating a text viewer to a rich-text or syntax-highlighted renderer. Preserve the same source string and compare the rendered result with highlighting enabled and disabled. If the only intended change is color, text order should not change with the highlighting state.
Keep relevant neighboring text in that comparison. Testing a highlighted value alone can conceal its interaction with the Arabic label or following punctuation. Include a search miss as well as a match: the DOM may be split differently when there is nothing to highlight.
Do not respond by making every new span inherit the parent's bidi mode. That repeats the scope on formatting fragments instead of preserving one boundary around the value. Locate the selector that assigned those fragments a mode, and remove that assignment where they have no directional role.
Debug the box, its ancestors and its source text#
Select the actual text-bearing element in DevTools. This inspection lists its mode and the modes on its ancestors:
const rows = [];for (let element = $0; element; element = element.parentElement) { const style = getComputedStyle(element); rows.push({ tag: element.localName, className: element.getAttribute("class"), dir: element.getAttribute("dir"), display: style.display, direction: style.direction, unicodeBidi: style.unicodeBidi });}console.table(rows);$0 is the element selected in DevTools. Use the Styles panel to locate the winning declarations; computed properties identify the outcome but not the stylesheet responsible. Disable one relevant declaration at a time and keep the surrounding sentence intact.
Compare source text with appearance. The supplied browser measurements show that copying text preserves logical order even when visual order differs. A successful paste of the packet reference into an editor therefore cannot prove that the console displayed it correctly. Keep a screenshot or visual assertion for that separate requirement.
If CSS inspection does not explain the behavior, check whether the source string contains directional control characters. Those are data, not computed CSS declarations. Do not delete them indiscriminately; identify where they entered the rendering pipeline and whether they are intentional.
For a broader diagnosis involving the document's direction rather than a local bidi mode, continue with dir="rtl" vs CSS direction: rtl.
Test the intended operation, not just a property value#
Use a small fixture set tied to the maintenance console:
| Fixture | What to verify |
|---|---|
Latin equipment marker PUMP beside its Arabic label | An RTL override has not reversed the letters |
Packet reference 975308-6421 after Arabic text | Its groups retain their documented identifier order |
Arabic plan title فحص المضخة الاحتياطية | A generic LTR override has not damaged normal reading |
| Text-only excerpt with Arabic and English paragraphs | Each paragraph gets the intended bidi processing |
| The same excerpt rendered as child paragraphs | Direction is declared at the new paragraph boundaries |
| Components matching a reset stylesheet | Neither an override survives unnoticed nor isolation disappears |
For each repair, keep a failing version until the fixture demonstrably exposes the defect. Then confirm the repaired version satisfies the visible reading requirement. Checking only that unicodeBidi equals isolate would miss an isolate with the wrong base direction or the wrong owned text.
Repeat at narrow widths to distinguish wrapping from paragraph boundaries. Update the plan title and event text without reloading. Check the English route and any print rendering that shares these classes. Run the fixtures in supported browsers and record versions with the visual evidence.
A free Ritla scan can report , forced-LTR container, or a bidi override, over Arabic content. For the next implementation decision, read nested LTR content inside an RTL page for known-direction regions or dynamic text direction for values whose direction can change.