Skip to content

RTL vs bidi: what's the difference?

Learn how RTL direction differs from bidirectional text ordering, which bugs belong to each, and how to test and fix both.

By , Founder of Ritla

Guide12 min read

An Arabic maintenance card can have its priority stripe on the wrong edge while the incident number inside the card appears in the wrong order. Those look like one vague “RTL issue” in a bug report. They are two different failures. One concerns where a box is placed. The other concerns how characters inside a box are displayed. Fixing the stripe will not fix the number, however earnestly you adjust its margin.

That is the practical difference between RTL and bidi. RTL describes a right-to-left direction used by a script, a text block, or a direction-aware layout. Bidi, short for bidirectional text, concerns the ordering of text that contains runs with different directional behavior. The Unicode Bidirectional Algorithm processes text in every paragraph; its effects become especially visible when Arabic meets Latin letters, Western digits, and punctuation. The ideas are connected, not competing settings you choose from a dropdown.

The distinction matters because it changes the repair. A misplaced card edge asks for direction-aware layout rules. A scrambled identifier asks for an appropriate text boundary. A correctly RTL page can still have a bidi failure, and an English LTR page can have one too.

Two questions, two layers#

When someone says “this is backwards,” ask what this refers to. Is the card on the wrong side of its grid? Or is the card correctly placed but the number printed inside it looks out of sequence? The screenshot can look similar until you inspect the box.

What you are checkingRTL direction and layoutBidi text ordering
Unit of concernA document, component, line box, or layout axisCharacters and directional runs within text
Typical symptomA border, panel, gap, or first item sits on the wrong physical sideDigits, punctuation, or a Latin phrase appear in the wrong visual order
Starting pointHTML dir, computed direction, flex/grid placement, CSS sidesParagraph direction, source text, value boundary, isolation
Typical repairDeclare the intended direction; use logical layout properties where the side follows reading flowIsolate a known-direction value or set its base direction in markup
What not to expectLogical CSS does not reorder characters in a value<bdi> does not move a card or mirror an icon

There is an important overlap: the HTML dir attribute affects both. It provides the base direction used for text ordering, and direction-aware CSS layout can use that direction to decide where inline start and end lie. The HTML dir reference explains the base direction; CSS logical properties explain flow-relative edges. One attribute can influence two layers without making those layers the same thing.

“RTL” also does not mean “mirror every pixel.” Text flow, a start-aligned status stripe, and sequential navigation may change sides. A logo, a graph of time increasing left to right, or a physical map may not. Those are product decisions. The mirroring guide covers them in detail; this page is about telling a text-order failure from a placement failure.

RTL establishes a base direction#

Arabic text is normally read from right to left. On an Arabic page, express that in the document rather than hoping each component guesses:

html
<html lang="ar" dir="rtl">

lang="ar" says what language the content is in. It does not turn the page RTL. In the Chromium 151 behavior checked for this guide, a page with lang="ar" and no dir still computes to LTR. R001 concerns document direction that is missing, contradicted, or declared only in CSS. Use the markup direction for a known Arabic page; the HTML direction guide explains the details beyond this comparison.

Once the document is RTL, normal layout behavior changes. In the checked Chromium behavior, the first item in an RTL flex row or grid starts at the right, and margin-inline-start maps to the right margin. A physical margin-left, left, or border-left remains physically left. This is why a component can inherit the correct dir and still look misplaced: its stylesheet may be written in physical sides that ignore the direction. R030 concerns direction-sensitive physical properties without an RTL override.

The base direction also gives the text algorithm a starting point. Punctuation and spaces often need that context to decide which run they belong to. Setting dir="rtl" is therefore not merely a layout switch. It influences the paragraph's text display too. But it does not reverse every Latin word, turn a fixed-order number into a safe identifier, or supply isolation for every embedded value. We will get to that shortly, after the stripe.

Bidi orders runs within the text#

The Unicode algorithm begins with characters in logical order, the order stored in the DOM and in your data. It assigns directional types: Arabic letters are strong Arabic RTL, Latin letters are strong LTR, Western digits are weak numeric characters, and characters such as spaces and many punctuation marks depend on context. It then resolves those weak and neutral characters, assigns levels to runs, and chooses a visual order for each displayed line. The Unicode standard's algorithm defines the process; you do not need to reproduce it in application code.

This is why “bidi” is more precise than “Arabic text mixed with English.” Mixed Arabic and English is a common place to notice bidi behavior, but the algorithm also processes a paragraph of only Arabic, a paragraph of only English, and one containing Arabic with numbers but no English letters. “Bidi bug” is a useful engineering label when the visual order of a mixed-direction value or its punctuation is wrong for the value's meaning.

A paragraph can be RTL while a Latin name remains LTR inside it. That is not a browser failure. It is how an Arabic sentence can contain a readable English name. The weak bits between runs are where surprises happen. The algorithm cannot tell whether a hyphenated pair of numbers is a range to be read in sentence order or an identifier whose group order must remain fixed. Both look like digits and punctuation to Unicode. The application knows the difference; the browser needs you to express it when natural bidi processing is not what the value requires.

Stored and displayed order are not interchangeable. Reversing the source string before rendering is the wrong repair: it corrupts what JavaScript reads, what the server stores, and what a user copies. W3C's logical-versus-visual ordering explanation makes that distinction explicit. The browser already owns visual reordering. Your job is to supply the boundaries and base directions it cannot infer from product semantics.

One card can have both failures#

Consider a streetlight maintenance card. Its design uses a colored stripe at the start edge, so an English card has the stripe on the left and an Arabic card should have it on the right. It also shows a fixed-order incident reference beside an Arabic label:

html
<article class="fault-card" lang="ar" dir="rtl">
  <h2>فحص مصباح شارع النخيل</h2>
  <p>رقم البلاغ 905-42</p>
</article>

There are two independent observations to make. First, with border-left in its CSS, the stripe stays on the physical left even though the card's inline start is the right. Second, in the checked Chromium behavior, the numeric portion following the Arabic label appears as 42-905 when described from left to right. That follows the brief's measured rule for a Western-digit A-B value after Arabic text: the groups exchange visual positions. The DOM still contains 905-42.

Whether the numeric display is wrong depends on the product. This value is an incident reference, so its two groups have a fixed order. An Arabic reader should be able to identify the same reference that an English reader, an API, and a support agent use. For a range, the same visual arrangement may still be read from the right in the intended “from A to B” order. A bug report should name the value type, not simply say that the digits look reversed.

Here is the physical-side rule that misplaced the stripe:

Problem

css
.fault-card {
  border-left: 4px solid #9b5726;
  padding-inline: 1rem;
}

Better

css
.fault-card {
  border-inline-start: 4px solid #9b5726;
  padding-inline: 1rem;
}

border-inline-start maps to the physical left in a horizontal LTR layout and the physical right in a horizontal RTL layout. The English stripe stays where it was. The Arabic stripe moves to the card's intended start edge. The incident reference inside the card is still a separate problem. CSS border placement has no jurisdiction over its hyphen, which is a slightly pompous but accurate way to put it.

Now repair the fixed-order value without changing the card's direction:

Problem

html
<p lang="ar" dir="rtl">رقم البلاغ 905-42</p>

Better

html
<p lang="ar" dir="rtl">رقم البلاغ <bdi dir="ltr">905-42</bdi></p>

The HTML bdi element isolates the identifier from the surrounding sentence. The explicit LTR direction states a known property of this reference. In the checked Chromium behavior, the groups now display as 905-42; the Arabic label stays RTL. A plain <span> without dir would not give the value the same isolation. W3C's inline bidi guidance explains why a tightly wrapped value is the right boundary.

Notice what we did not do. We did not set the whole card to LTR. That would make the Arabic label and other descendants inherit an unsuitable base direction. We also did not use row-reverse to rearrange the card: the incident reference's character order is not a flex-item ordering problem. The two repairs each act at the layer where the failure occurs.

A useful distinction: direction versus isolation#

An element can have a base direction without its text being safely separated from neighboring runs. This matters for values inserted into a sentence. In HTML, a dir attribute on an element sets its direction and isolates its contents; <bdi> creates an isolated boundary and defaults to choosing direction from its own content when no explicit value is supplied. W3C's markup guidance covers both. The key practical point is to wrap the value, not the whole translated sentence, when the value needs a stable directional unit.

dir="auto" is useful when the content direction is unknown, such as a user-entered comment. It chooses direction from the first strong character. It is not a language detector. In the checked Chromium behavior, a value made only of Western digits and punctuation has no strong letter and resolves LTR when isolated with dir="auto" or <bdi>. That can happen to suit an identifier, but if your product contract says the identifier is LTR, say so explicitly. A comment that starts with a Latin brand name and continues in Arabic is an edge case where “first strong” may choose a base direction you did not expect.

The opposite case also matters: an English page can contain an Arabic name or note. That page remains LTR, while the inserted Arabic run may need its own direction and isolation. The maintenance system's English audit trail, for example, might show an Arabic inspector name:

html
<p lang="en" dir="ltr">Reviewed by <bdi lang="ar" dir="rtl">هدى ناصر</bdi>.</p>

The English sentence keeps its LTR base direction; the name is one isolated RTL value. Whether the punctuation would misbehave without that boundary depends on the exact surrounding text, so check the actual rendered audit message instead of inferring a failure from this small fixture. The important point is that the English route still needs a mixed-text case. Bidi is not an Arabic-route-only test, and forcing the whole audit trail RTL to accommodate one name would move the problem to every other entry.

Scope dir="auto" just as carefully. Putting it on a whole sentence containing an Arabic label and a numeric reference lets the label's first strong letter choose RTL for the whole sentence; it does not give the numeric reference an independent boundary. If the unknown-direction material is a user-entered name, isolate that name. If the reference's direction is known, mark that reference explicitly. The browser cannot separate product fields that your markup has merged into one anonymous text run.

There is a limit to the bdi dir="ltr" recommendation. Arabic-Indic digits have a different bidi class from Western digits. In the checked Chromium behavior, spaces between Arabic-Indic digit groups resolve right to left even inside <bdi> or an element with dir="ltr"; a narrowly scoped override may be needed when that exact spaced notation has a fixed LTR order. That is a specialized case, not permission to scatter overrides over normal Arabic text. The bidi guide goes into those value-level choices. This page's rule is simpler: distinguish the text boundary from the layout direction before choosing either repair.

CSS direction is not a complete substitute for markup#

It is possible to make text look RTL with CSS direction: rtl while leaving HTML direction unspecified. That may hide the missing document declaration in a screenshot. But direction is part of the content's meaning, and HTML should carry it. W3C recommends directionality in markup for HTML content rather than relying on styling to supply it.

There is a practical selector trap too. In the checked Chromium behavior, CSS direction: rtl without a dir attribute displays RTL, but :dir(rtl) does not match that element. The :dir() reference explains that it follows semantic directionality, not CSS-only direction. If an RTL-specific rule mysteriously fails while the text seems to point right, inspect the markup before adding another override. The dir versus CSS direction guide treats that distinction in full.

Neither text-align: right nor CSS logical properties set a known-direction value's isolation. text-align moves line content within a box. Logical margins, padding, borders, and insets place boxes and edges relative to the writing flow. They are useful, but they do not tell the UBA that a particular number is one fixed-order identifier. Likewise, bdi cannot fix a physical border-left or a panel translated off-screen. Put the remedy where the browser made the decision.

Diagnose the symptom before editing#

A quick two-pass inspection keeps the layers separate.

First, inspect the box. Select the element in DevTools and look at its highlighted rectangle. Is the whole card, stripe, button, or panel on the wrong physical side? Check the root and component dir, then the winning layout rules. Look for left, right, physical margin or border sides, and horizontal transforms. In an RTL flex or grid container, check whether the browser has already placed the first item at the right before you add row-reverse. R081 concerns row-reverse used to fake RTL when tab order fights the visual order. The RTL CSS guide covers direction-aware layout patterns beyond this triage.

Second, inspect the characters. If the box is where it belongs, compare the source string with what a reader sees. Is the value after an Arabic label, alone in a cell, or isolated inside its own element? Does it begin with Western digits, Latin letters, or Arabic-Indic digits? Are spaces, hyphens, and punctuation inside the value's boundary? Those details can change the result even when dir="rtl" is correct. A copy operation returns logical order, so it cannot certify visual order. Read the rendered value and test the same content in the actual context.

If both layers fail, file two observations. “The status stripe stays left because of border-left” and “the incident reference groups change order after the Arabic label” are actionable. “RTL broken” asks the next engineer to rediscover both. It also invites them to fix the easier-looking one and close the ticket while the number remains wrong.

Watch for a third category: icons and interaction. An SVG arrow does not pass through the Unicode text algorithm. A control that advances a sequence may need a direction-aware icon; a media play symbol may not. The card's keyboard order follows its DOM and interaction logic, not the visual order of letters inside its title. These are neither bidi isolation bugs nor necessarily paragraph-direction bugs. Label them accurately so the fix does not disturb text that was already correct.

Verify each repair independently#

For the stripe, compare the same card in LTR and RTL after the CSS change. The start-edge accent should be left in LTR and right in RTL, with the card's padding and content otherwise unchanged. Inspect the computed border side and test at a narrow width too; a breakpoint rule can reintroduce a physical side. R030 is relevant only if a direction-sensitive physical property remains without an RTL override.

For the reference, keep the stored value constant and check three render contexts: after its Arabic label without isolation, alone in an RTL text block, and after the label with the isolated LTR span. In the measured Chromium case, the 905-42 groups appear as 42-905, 905-42, and 905-42 respectively. A screenshot or human visual check is needed; textContent, copying, and a screen-reader pass preserve logical order and will not catch the visual group change. Check the result in your supported browsers rather than assuming this one measurement covers them.

Then repeat with a real phone number, email, or Latin token if your card displays one. R021 concerns those unisolated hazards inside Arabic text. It does not claim to detect every numeric incident reference, so keep the reference's visual fixture even if a scan is clean. If punctuation appears at the wrong visual end of Arabic text, R024 covers that separate symptom. This is where an Arabic reader's judgment matters: the goal is readable meaning, not making the pixels resemble the English version.

A free Ritla scan can flag the physical-property and mixed-text problems described by the relevant checks, giving you places to inspect. But the useful question at each finding remains the same: did the box move to the wrong edge, or did the text inside it acquire the wrong visual order? Once you can answer that, “RTL versus bidi” stops being terminology and starts saving you from the wrong fix.

Checks in this guide

See what your Arabic pages are hiding

Paste a URL. Ritla renders the page on desktop and mobile, runs every check, and shows the top issues with screenshot evidence.

Scan a page freeBrowse the checks