dir="rtl" vs CSS direction: rtl

Learn why HTML dir belongs in Arabic markup, how CSS direction behaves differently, what CSS-only RTL breaks and how to test the migration.

Guide14 min read

Both dir="rtl" and direction: rtl can make an Arabic block appear right to left. That visual similarity hides a real platform difference. The HTML attribute declares the content's base direction. The CSS property changes rendering as a style.

The difference shows up as soon as code asks a question other than “which side is this text aligned to?” An element styled with direction: rtl can compute to RTL while failing to match :dir(rtl). A direction set on an HTML table column through CSS does not inherit into cells the way the HTML attribute does. If the stylesheet is unavailable, CSS-only directional information disappears.

For an Arabic page, put dir="rtl" on the root HTML element. Use CSS logical properties to make layout respond to that direction. Do not use the CSS property as the document's source of truth.

The short answer#

Use this for an Arabic document:

html
<!doctype html>
<html lang="ar" dir="rtl">
  <head>
    <meta charset="utf-8">
    <title>بوابة القبول</title>
  </head>
  <body>...</body>
</html>

Do not replace the attribute with this:

css
html[lang="ar"] {
  direction: rtl;
}

The CSS may make the rendered page look RTL, but the markup still does not declare its directionality. The W3C guidance for structural RTL markup says to set document direction with dir and not CSS. MDN's direction reference gives the same warning for HTML content.

Keep the responsibilities separate:

ConcernUse
Document or content base directionHTML dir attribute
Content languageHTML lang attribute
Spacing and positioning that follow directionCSS logical properties and values
Styling an element because its resolved direction is RTLCSS :dir(rtl) pseudo-class
Unknown user-content directiondir="auto" on the content boundary
Opposite-direction inline valueA tight dir boundary or <bdi>

The CSS direction property still exists and affects layout. That does not make it the right API for HTML document semantics.

Base direction is content information#

Base direction tells the Unicode Bidirectional Algorithm how to order directional runs and resolve neutral characters such as spaces and punctuation. It also influences default alignment, the flow of table and grid columns, form control direction and horizontal overflow.

That information belongs with the content. The HTML dir global attribute accepts ltr, rtl or auto. When it is absent, direction generally inherits from the parent; when no ancestor supplies it, the default is LTR.

Language is different. lang="ar" identifies Arabic, but it does not make the element RTL. This page is wrong:

html
<html lang="ar">
  <body>...</body>
</html>

In Chromium 151, measured on 2026-09-24, the page still lays out left to right and its computed direction is ltr. Add dir="rtl" explicitly.

The distinction matters for multilingual content too. English is normally LTR, but an English page can quote an RTL block. Arabic is normally RTL, but an Arabic interface can contain a deliberately LTR machine identifier. Language and direction often correlate; neither should be inferred from the other in product markup.

CSS can render RTL without declaring HTML direction#

Consider an Arabic university application portal that chooses styles from a locale class.

Problem

html
<html lang="ar" class="locale-ar">
  <body>...</body>
</html>
css
.locale-ar {
  direction: rtl;
}

The page can align Arabic text to the right, place the first flex item at the right edge and compute direction: rtl. A visual check may pass.

The markup still has no dir attribute. Direction disappears if the relevant CSS is missing, delayed, replaced or excluded from a consuming context. Tools that inspect the semantic attribute cannot find it. A :dir(rtl) selector does not match merely because the CSS property says RTL.

Better

html
<html lang="ar" dir="rtl">
  <body>...</body>
</html>
css
.application-summary {
  padding-inline: 1rem;
  text-align: start;
}

The root attribute establishes direction before component CSS loads. Logical CSS then maps start and end from that direction. The English route can use dir="ltr" with the same component stylesheet, so the LTR geometry remains intact.

R001 reports document direction that is missing, contradicted or declared only in CSS. A computed-style assertion alone cannot catch the CSS-only case because the computed property is exactly what made the page look correct.

:dir(rtl) does not mean direction: rtl#

The :dir() pseudo-class selects an element by its document directionality, including inherited direction. It does not select based on a stylistic state. Selectors Level 4 states explicitly that the CSS direction property does not determine whether :dir() matches.

This produces a useful diagnostic:

html
<section class="css-only">...</section>
<section dir="rtl" class="markup-direction">...</section>
css
.css-only {
  direction: rtl;
}

section:dir(rtl) {
  border-inline-start: 0.25rem solid currentColor;
}

Both sections can compute to direction: rtl. Only the section with HTML directionality matches :dir(rtl) in the measured browser.

Do not work around that by replacing :dir(rtl) with a class everywhere. Fix the source of direction. A locale class may still be useful for typography or market-specific styling, but it should not substitute for dir.

An attribute selector has a different limitation:

css
[dir="rtl"] .timeline {
  /* Matches descendants of an explicit RTL boundary. */
}

.timeline:dir(rtl) {
  /* Matches the timeline when its resolved HTML directionality is RTL. */
}

[dir="rtl"] looks for a literal attribute on an ancestor matching that selector. :dir(rtl) asks for the element's directionality and therefore handles inherited or locally overridden direction. Prefer the one that represents the condition you mean.

HTML and CSS inheritance are not identical#

On ordinary descendants, both mechanisms can appear to inherit in the same way. Edge cases expose the different models.

CSS inheritance follows the document tree. A table cell is a descendant of its row, not its column. MDN notes that a CSS direction value on a table column does not inherit into cells, while HTML dir has special table directionality behavior. A team can style a <col> and assume the corresponding cells received its direction when they did not.

That is a reason to keep content direction in HTML rather than trying to reconstruct HTML's rules through CSS selectors.

Direction also crosses component boundaries differently depending on where the boundary is mounted:

  • an ordinary descendant inherits from the closest HTML direction boundary;
  • content in a shadow tree can inherit direction from its host;
  • a portal mounted under body does not inherit from a nested application container it has left;
  • a third-party widget may reset or override direction inside its own root.

Putting dir on html gives dialogs, popovers and portals attached to the document body the correct default. If only #app carries the attribute, a body-level portal can fall back to another ancestor's direction.

Test the actual mount point. A component story inside an RTL wrapper proves the component can inherit direction. It does not prove the production portal receives the same ancestor.

Inline content exposes another difference#

The CSS property is particularly easy to misuse on inline elements. MDN notes that direction has an effect on an inline-level element only when unicode-bidi creates an embedding or override. A developer can add direction: ltr to a span, see no reliable isolation of the phrase and then escalate to unicode-bidi: bidi-override.

That escalation is dangerous. An override tells the bidi algorithm to disregard the intrinsic direction of characters inside the run. It can reverse content that was already ordered correctly, and it can make identifiers or punctuation appear plausible while no longer reflecting logical text order.

Use markup that represents the content boundary instead:

html
<p dir="rtl">
  تمت مراجعة برنامج
  <cite lang="en" dir="ltr">Data Systems!</cite>
</p>

The English title is tightly wrapped, its language is identified, and its final punctuation stays inside the same directional boundary. The Arabic paragraph keeps its own base direction.

For an injected value with unknown direction, use <bdi> or dir="auto" on the smallest complete value. For known LTR content, declare dir="ltr". Do not use a CSS class that sets direction plus unicode-bidi as a generic substitute for all three cases. Known direction, unknown direction and forced character override are different operations.

The edge case is an inline whose direction is purely a rendering concern in a non-HTML format. That can justify the CSS property in that format. Once the content is HTML, the markup API communicates the boundary more accurately.

Direction changes more than alignment#

CSS-only RTL often begins as an attempt to move Arabic text to the right. text-align: right is an even weaker substitute: it moves line boxes without establishing a right-to-left base direction. Mixed runs and punctuation still resolve from the existing direction.

The base direction affects several observable behaviors:

  • neutral punctuation at the boundaries of mixed runs;
  • the ordering of directional runs inside a paragraph;
  • default start alignment;
  • the flow of table and grid columns;
  • horizontal overflow direction;
  • inherited direction for form controls, with type-specific exceptions;
  • the direction used for natural-language attributes such as title and alt in relevant HTML contexts.

This is why a screenshot of one Arabic paragraph is a poor equivalence test. Two implementations can align that paragraph identically while differing on a table, an application reference, a tooltip or an overflow case.

Build a compact comparison fixture when migrating a product. Include one Arabic paragraph with punctuation, one fixed-order identifier, one table, one text input, one opposite-direction quote and one intentionally overflowing box. Render the same fixture with the attribute implementation and the old CSS-only implementation. The differences show which component assumptions must be removed.

Do not turn that fixture into a universal browser claim. Run it in the browsers the product supports, and store the expected result with the test suite. The purpose is to expose the product's dependency on direction, not to recreate the platform conformance suite.

The attribute survives without presentation CSS#

W3C's practical reason for putting direction in markup is that the directional information remains available when CSS is not. This is not merely an accessibility thought experiment.

Styles can fail or be absent in:

  • a partially loaded page;
  • HTML shown by a simplified reader or consuming service;
  • a test that inspects markup before CSS is applied;
  • a print or export pipeline with a reduced stylesheet;
  • content copied into another rendering surface;
  • an error document that does not load the main bundle.

Not every consumer will honor every HTML behavior identically, but attaching direction to the content gives it information to work with. CSS-only direction guarantees that any consumer without that rule loses the declaration.

Avoid overstating this point. dir does not make an unstyled application usable, and it does not replace logical layout CSS. It preserves the base direction while presentation changes or disappears.

dir and CSS can contradict each other#

CSS can override the rendering established by an HTML direction attribute. This is one reason the two mechanisms should not be maintained as parallel sources of truth.

html
<main dir="rtl" class="application-shell">...</main>
css
.application-shell {
  direction: ltr;
}

The markup declares RTL while computed style becomes LTR. Code using :dir(rtl) can still treat the element as RTL, while layout follows the CSS property. The application is now internally contradictory.

Search for global resets and component rules that set direction. The CSS all shorthand does not reset direction or unicode-bidi, so a broad all: unset is not a reliable way to clear an inherited directional override. Remove the unwanted rule or set the correct content boundary in markup.

R001 includes contradicted document direction. R020 reports a forced-LTR container, or a bidi override, over Arabic content. Use the first for the document contract and the second for a content container that forces Arabic into an LTR or override context.

Do not patch the contradiction with !important. That makes the cascade harder to inspect while leaving two competing declarations in the codebase.

Put dir at the highest correct boundary#

For a predominantly Arabic document, set dir="rtl" on <html>. Descendants inherit it, so repeating the attribute on body, every section and every component adds noise.

Add a nested boundary only when that content has a different known base direction:

html
<html lang="ar" dir="rtl">
  <body>
    <article>
      <p>...</p>
      <blockquote lang="en" dir="ltr">...</blockquote>
    </article>
  </body>
</html>

The blockquote declares both its English language and LTR direction. Its direction does not need a CSS class.

For an inline value, wrap the smallest complete unit. An application reference with fixed group order should not borrow direction from the surrounding Arabic sentence:

html
<p dir="rtl">رقم الطلب <bdi dir="ltr">8157-42-906</bdi></p>

The boundary belongs around the value, not the entire label-and-value component. R021 reports unisolated bidi hazards such as phones, emails and Latin tokens inside Arabic text. The bidirectional text guide covers inline isolation in more depth.

Use dir="auto" for unknown content, not the page#

dir="auto" asks the browser to determine base direction from the first strong directional character. It is useful for user-generated content when the application does not know the language in advance.

html
<article class="applicant-note" dir="auto">
  <!-- User-provided text can begin in Arabic or English. -->
</article>

It is a heuristic, not language detection. A note that begins with a Latin product name and continues in Arabic resolves LTR because the first strong character is Latin. If the application knows the content is Arabic, use dir="rtl" instead of asking the browser to guess.

Do not put dir="auto" on the root of an Arabic product. The direction could vary with the first strong character in a title or loading state, which makes the document contract unstable.

Form controls need an edge-case check. An empty input with dir="auto" resolves LTR, so an Arabic placeholder can begin at the left edge until Arabic is entered. Auto direction suits genuinely unknown user input. Known Arabic fields should inherit RTL, while email, URL and phone fields should remain LTR according to their value type.

CSS still controls directional layout#

Choosing the HTML attribute does not remove direction from CSS layout. It gives CSS a correct input.

Use logical properties and values:

css
/* --status-color is defined by the application's theme. */
.application-card {
  border-inline-start: 0.25rem solid var(--status-color);
  margin-inline: 0 auto;
  padding-inline: 1rem;
  text-align: start;
}

.application-card__action {
  inset-inline-end: 1rem;
}

Start and end resolve from writing mode and direction. The same rules preserve the English layout and produce its RTL counterpart without duplicating every declaration.

The browser does not mirror physical properties, transform distances, SVG drawings or bitmap assets. Continue to audit left, right, physical margins, shadows, animations and directional icons. R030 reports direction-sensitive physical properties without an RTL override.

Use :dir(rtl) when the component truly needs a directional variant:

css
.step-icon:dir(rtl) {
  transform: scaleX(-1);
}

Do not flip a whole component to reverse one icon. Text and non-directional assets can be mirrored accidentally. Decide the semantic asset behavior first, then scope the transform to that asset.

When CSS direction is appropriate#

For HTML content, the answer is narrow: do not use the CSS property as the canonical base-direction declaration.

There are still legitimate technical contexts for the property:

  • non-HTML rendering vocabularies that define direction through styling, such as directional SVG text;
  • diagnostic work in DevTools where you temporarily change computed direction;
  • constrained third-party surfaces whose API exposes styles but no semantic direction option;
  • low-level components that must inspect or reset an external style contract before proper markup can be added.

Treat the last two as integration constraints, not patterns to spread through the application. Document the boundary, cover it with a browser test and migrate to dir when control of the markup becomes available.

CSS direction also participates in behaviors beyond text alignment, including table and grid column flow and horizontal overflow. That makes casual use risky. A developer trying to align one label can change a component's broader directional behavior. Use text-align for an intentional alignment override; do not reach for direction as an alignment shortcut.

Update direction during locale changes#

In a client-rendered application that switches locale without navigation, update language and direction together on the document element.

js
const localeDirection = {
  ar: "rtl",
  en: "ltr",
};

export function applyDocumentLocale(locale) {
  const language = locale.split("-")[0];
  const direction = localeDirection[language] ?? "ltr";

  document.documentElement.lang = locale;
  document.documentElement.dir = direction;
}

The map is a product contract, not a universal list. Add every supported RTL language explicitly. Do not determine direction by checking whether translated text happens to contain Arabic characters.

Call the function before rendering the new locale where the framework lifecycle permits it. Then test:

  • initial deep links in each locale;
  • Arabic to English and English to Arabic;
  • navigation after switching;
  • authentication redirects;
  • error boundaries and full-page fallbacks;
  • portals already open during the switch;
  • persisted locale after refresh.

Server-rendered applications should emit the correct attributes in the response rather than relying on this client function to repair the root after hydration.

Test markup and rendering separately#

A good test suite asserts both the semantic declaration and the rendered result.

js
// page and expect are supplied by the browser test runner.
await page.goto("/ar/applications/status");

const root = page.locator("html");
await expect(root).toHaveAttribute("lang", /^ar(?:-|$)/);
await expect(root).toHaveAttribute("dir", "rtl");

const rootState = await root.evaluate(element => ({
  computed: getComputedStyle(element).direction,
  matchesDir: element.matches(":dir(rtl)"),
}));

expect(rootState.computed).toBe("rtl");
expect(rootState.matchesDir).toBe(true);

Each assertion catches a different failure:

  • the attribute assertion rejects CSS-only direction;
  • the computed-style assertion catches a contradictory CSS override;
  • the pseudo-class assertion confirms the selector state components rely on.

Continue with behavior that matters to the product: mixed-direction fixtures, table order, field direction, logical spacing, overflow and keyboard sequence. Do not infer all of that from a single root check.

Inspect the initial server response too. A browser test that waits for hydration can miss markup that begins LTR and is corrected later.

Debug direction from the boundary inward#

When a component appears on the wrong side, avoid changing CSS until you know which direction source it received. Alignment, flex order and text direction can make the same symptom for different reasons.

Use this sequence in DevTools:

  1. Select the broken element and read its computed direction.
  2. Run $0.matches(":dir(rtl)") in the console. DevTools exposes the selected element as $0.
  3. Walk to the nearest ancestor with a dir attribute, including the document root.
  4. Inspect matched rules for direction and unicode-bidi.
  5. Check whether the element lives in a portal, shadow tree or third-party root.
  6. Disable the suspected CSS direction rule and observe whether markup direction remains.
  7. Inspect the physical property or transform that controls the misplaced geometry.

Interpret the combinations carefully:

HTML boundaryComputed property:dir(rtl)Likely condition
dir="rtl"rtlYesMarkup and rendering agree
No RTL boundaryrtlNoCSS-only direction
dir="rtl"ltrYesCSS contradicts markup
No RTL boundaryltrNoDirection was never established

The table describes the measured HTML behavior relevant to this guide. A local dir="ltr" boundary, shadow host or non-HTML element can change the diagnosis, so inspect the actual ancestry rather than applying the table mechanically.

After direction is correct, debug layout separately. If a card still sits on the wrong edge, look for left, right, physical margins, row-reverse, translate transforms or an asset that needs a directional variant. Changing base direction repeatedly will not repair a physical coordinate.

Capture the cause in the regression test. For a missing attribute, assert the attribute. For a contradictory rule, assert both the attribute and computed style. For a physical layout bug, test the component geometry or visual state. One broad RTL screenshot makes future failures harder to classify.

Migrate a CSS-only Arabic page safely#

Moving direction into markup can expose CSS that was compensating for the old setup. Make the migration observable.

  1. Add dir="rtl" to the Arabic document root and dir="ltr" to the LTR root when explicit symmetry helps the application.
  2. Keep lang accurate and independent from direction.
  3. Remove the root CSS direction rule so one source remains.
  4. Search for component-level direction, unicode-bidi, physical side properties and row-reverse patches.
  5. Replace writing-direction relationships with logical CSS.
  6. Audit [dir] selectors and decide whether they require a literal boundary or resolved :dir() state.
  7. Test tables, grids, fields, mixed values, overflow and body-level portals.
  8. Compare the same states in LTR and RTL at supported viewports.
  9. Verify the initial response and the hydrated application.

Watch for double mirroring. A component that reversed its row because the old root stayed LTR may reverse again after proper RTL inheritance. Remove the compensation rather than adding another override.

Also check CSS modules and shadow roots. A selector that depended on .locale-ar can stop applying even though direction is now correct. Replace it with :dir(rtl) only when the variation is truly directional; keep the locale class if the rule represents language-specific typography or market behavior.

Make markup the source of truth#

dir="rtl" and direction: rtl can produce similar pixels, but they do not create the same document state. The HTML attribute carries content direction, participates in HTML's directionality rules and drives :dir(). CSS should consume that state through logical layout and tightly scoped directional variants.

A Ritla scan can show when document direction is missing, contradicted or present only in CSS, which gives the migration a precise starting point. Continue with the HTML dir="rtl" guide for document setup, then use the RTL CSS guide to remove the physical layout assumptions that proper markup exposes.

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.