How to handle dynamic text direction

Handle Arabic, English and mixed user content at runtime with dir auto, stored direction metadata, safe rendering and repeatable browser tests.

Guide15 min read

A customer-support inbox cannot assign one direction to every message. One customer writes Arabic, another writes English, and a third starts an Arabic reply with a Latin product name. If the message body only inherits the interface direction, some messages align and punctuate incorrectly. If application code guesses from the first character, hashtags, emoji and numbers create a different set of failures.

Use three levels of direction instead:

  • keep the application shell in the locale's known direction;
  • apply stored direction metadata to content when the author or source provides it;
  • use dir="auto" on the smallest unknown text container as a fallback.

Direction belongs to the string being displayed. It should not leak into the message controls, timestamp, assignee menu or the rest of the page.

Keep the page direction stable#

Dynamic content does not make the document direction dynamic. An Arabic support interface still starts with an Arabic root:

html
<!doctype html>
<html lang="ar" dir="rtl">
  <head>
    <meta charset="utf-8">
    <title>صندوق الدعم</title>
  </head>
  <body>...</body>
</html>

The root direction controls the shell: navigation, panels, tables and the default flow of Arabic interface text. A message body can override that direction locally without changing the document.

Do not set the root to auto because the route contains user content. The first strong character in the document could come from a product name, a loading state or an injected message. Page direction is known from the locale, so declare it. R001 reports document direction that is missing, contradicted or declared only in CSS.

The W3C structural direction guidance recommends setting the document's base direction on the html element, then using auto for forms and inserted text whose direction is not known until runtime.

Understand what dir="auto" decides#

dir="auto" asks the browser to choose a base direction from the content. It generally scans for the first character with strong directionality. An Arabic letter resolves RTL; a Latin letter resolves LTR. Leading whitespace, digits and most punctuation do not make the decision.

html
<p dir="auto">تعذر إرفاق المستند.</p>
<p dir="auto">The document could not be attached.</p>

Each paragraph gets a direction based on its own text. The surrounding interface may remain RTL.

This is not translation detection, language detection or a count of scripts in the string. The browser does not decide which language dominates the paragraph. It finds an early directional signal and uses that as the base for the whole element. MDN's dir reference documents this first-strong behavior and recommends auto for external or user-supplied data with unknown direction.

Omitting dir is not equivalent. An element with no direction attribute normally inherits from its parent. In an Arabic interface, an unknown English message therefore inherits RTL even though its letters still form LTR runs. The base direction still affects alignment, punctuation and the ordering of those runs.

Use auto when the direction really is unknown. Use explicit rtl or ltr when the content contract already tells you the answer.

Put automatic direction on the content, not the card#

The element with dir="auto" should contain one independently authored string. Static interface text outside that string must not participate in detection.

Problem

html
<article class="conversation-item" dir="auto">
  <p class="conversation-item__label">NOVA Support</p>
  <p class="conversation-item__body">تعذر إكمال التحقق.</p>
  <button type="button">تعيين لموظف</button>
</article>

The Latin label is the first strong text in the article, so it makes the entire component LTR. The Arabic message never gets a chance to establish its own base direction. The action is also pulled into a direction decision that belongs only to the message.

Better

html
<article class="conversation-item">
  <p class="conversation-item__label">NOVA Support</p>
  <p class="conversation-item__body" dir="auto">
    تعذر إكمال التحقق.
  </p>
  <button type="button">تعيين لموظف</button>
</article>

Now the shell follows the page and the body resolves independently. The same component structure works on the English route because the boundary is attached to the unknown value rather than to one locale's layout.

Apply the same rule to a search result. The result title and excerpt may each come from a different source, so each needs its own direction boundary. Putting auto on the result card lets whichever field appears first decide direction for all of them.

Know where first-strong detection fails#

First-strong is a useful fallback, not an authorial decision. It fails when the first strong character does not represent the direction of the string as a whole.

An Arabic message may start with a Latin campaign tag:

html
<p dir="auto">#NOVA تعذر تطبيق رمز الحملة.</p>

The first strong character is the N, so automatic direction chooses LTR even though the sentence is Arabic. A leading URL, username or product name can produce the same mismatch. The browser has followed the algorithm; the content needs better metadata.

If the authoring surface knows the message is Arabic, render that knowledge:

html
<p lang="ar" dir="rtl">#NOVA تعذر تطبيق رمز الحملة.</p>

Directionless values are another edge case. An identifier made only of digits and punctuation contains no strong letter. In Chromium 151, checked on 2026-09-24, an automatic boundary containing only such a value resolves LTR. That result may happen to fit a technical identifier, but it does not tell you what the value means. If a field is contractually an LTR reference, say so explicitly:

html
<code dir="ltr">742-681-35</code>

Emoji and punctuation at the beginning are skipped until the algorithm reaches a strong character. That helps a message such as the following resolve from its first Arabic letter:

html
<p dir="auto">📎 تم إرفاق سجل المحادثة.</p>

Do not replace the browser behavior with a regular expression that checks whether the first code point falls in an Arabic range. That test mishandles whitespace, emoji, combining marks and scripts beyond the range its author remembered. It also cannot recover the author's intent when a Latin tag precedes an Arabic sentence.

The W3C article on strings and bidi shows the same limitation: isolation plus first-strong detection fixes many inserted strings, but a title beginning with a strong character from the other direction still needs explicit information.

Store direction with content when intent matters#

If users can choose direction, or an upstream system already knows it, persist that decision beside the text. Do not make every client guess again.

json
{
  "body": "#NOVA تعذر تطبيق رمز الحملة.",
  "language": "ar",
  "direction": "rtl"
}

Keep the values limited to ltr, rtl and auto. Treat missing metadata as auto, not as the current viewer's locale. A message written in Arabic remains RTL when an English-speaking agent opens it.

The rendering layer maps the field to HTML without putting markup inside the stored string:

js
// message is the record returned by the support API.
const allowedDirections = new Set(["ltr", "rtl", "auto"]);
const body = document.querySelector("[data-message-body]");

body.textContent = message.body;
body.dir = allowedDirections.has(message.direction)
  ? message.direction
  : "auto";

if (message.language) {
  body.lang = message.language;
} else {
  body.removeAttribute("lang");
}

Using textContent keeps content separate from markup. The allowlist prevents an arbitrary API value from becoming an invalid direction contract. If the message has no known language, omit lang; do not copy ar from the interface just because the agent is viewing the Arabic route.

W3C's string metadata guidance distinguishes string direction from document direction and recommends carrying direction metadata with natural-language strings instead of depending on heuristics alone. It is current working-draft guidance, not a reason to invent metadata for internal codes or other non-linguistic values.

Decide which layer owns the field. If the API names it direction, use the same meaning in the database, events and clients. Avoid one service sending isRtl, another inferring from locale, and a third writing a CSS class. The disagreement becomes visible when content moves between the message list, search index, export and notification system.

Keep presentation controls out of the canonical string#

An invisible direction mark at the beginning of a string can influence first-strong detection, but storing one inside every message is a poor substitute for metadata. The control becomes part of the value. It affects length checks, exact comparison, cursor movement, search queries and exports even though reviewers cannot see it.

Keep the authored text and its direction separate:

json
{
  "body": "#NOVA تعذر تطبيق رمز الحملة.",
  "direction": "rtl"
}

The HTML renderer can apply dir="rtl"; a mobile client can map the same field to its native text API; an export can decide how its target format represents direction. None of those consumers has to remove a hidden character before comparing the message text.

Plain-text destinations are the edge case. A push notification, terminal transcript or text-only export has no HTML element on which to put dir. It may need Unicode isolate controls around an inserted run, applied by the serializer for that destination. W3C's guidance on Unicode bidi controls explains their role when markup is unavailable and still recommends markup for document and block-level direction.

Do not add controls to the database simply because one destination needs them. Generate them at that boundary, and test the exact receiving application because plain-text bidi support is not identical across every channel. Document whether exports contain controls so downstream systems do not treat visually identical strings as byte-for-byte equal.

User-authored text can already contain directional controls, deliberately or through pasted content. Do not silently strip them from every message without a product decision. Instead, make them visible in moderation and debugging tools, preserve the untouched source where required, and ensure the display layer still isolates the complete user string from surrounding interface text.

Migrate legacy records without freezing a guess#

An existing message table may have millions of strings and no direction column. Adding direction does not require classifying every old row as permanently RTL or LTR.

Use a nullable or auto default for unknown legacy data. Resolve it at display time, then store an explicit value only when a trusted source supplies one or an author changes it. A backfill based on first-strong detection merely writes the browser's fallback into the database and makes later correction harder.

Define the API states deliberately:

json
{
  "body": "تم تحديث جهة الاتصال.",
  "direction": "auto",
  "language": null
}

Decide whether null and auto mean the same thing. If they do, normalize at the API boundary. If null means “legacy unknown” and auto means “the author selected automatic,” keep both states documented so an edit form does not overwrite one with the other accidentally.

Search indexes and event payloads need the field too when they render results independently. A direction stored only in the primary database is lost if the search service returns plain title strings. Include metadata in projections that produce user-visible text, and add a contract test that follows one explicitly RTL, Latin-leading message from creation through indexing to the result page.

Capture form direction with dirname#

HTML can submit the resolved direction of an input or textarea along with its value. The feature is easy to miss because dirname does not change the field's appearance; it names an additional form field.

html
<form method="post" action="/support/replies">
  <label for="reply">الرد</label>
  <textarea
    id="reply"
    name="reply"
    dir="auto"
    dirname="reply.direction"
  ></textarea>
  <button type="submit">إرسال الرد</button>
</form>

The submission includes the reply text and a second value named reply.direction, whose value is ltr or rtl. MDN's dirname reference documents this behavior for textarea and supported input types.

This gives the server the field direction at submission time. It does not identify the language, and it does not prove that first-strong matched the author's intent. If the editor offers an explicit direction control, submit and store that selected value instead of silently replacing it with a fresh guess.

API submissions built with JavaScript do not automatically acquire the extra field unless the code reads it from form data or sends an equivalent property. Test the actual request payload. A working native form example does not prove that a custom client serializer preserves dirname.

Treat submitted direction as display metadata, not authorization or trusted HTML. Validate it against the three allowed values and escape or sanitize the text according to its content type.

Make the editor follow the value being typed#

For a free-form reply that may be Arabic or English, dir="auto" lets the editing direction change with the text:

html
<label for="internal-note">ملاحظة داخلية</label>
<textarea
  id="internal-note"
  name="internalNote"
  dir="auto"
  placeholder="اكتب ملاحظة للفريق"
></textarea>

A related empty-state edge case appears in single-line fields. In Chromium 151, an empty input dir="auto" resolves LTR, so an Arabic placeholder is drawn at the left edge. Typing an Arabic letter changes the field to RTL; typing a Latin letter or digits keeps it LTR. Firefox and Safari were not part of that measurement, so check the browsers in your support matrix.

If the Arabic placeholder must sit at the RTL start before input, use a surrounding label or hint that inherits the interface direction, or give the editor an explicit initial direction and provide a user control to switch it. Do not add JavaScript that flips direction on every keystroke without preserving the user's choice. That produces caret movement when a reply starts with a case code or pasted URL.

Typed fields are different from known technical fields. A reply editor may use auto; an email address, callback URL or fixed reference has an LTR content contract and should use dir="ltr". R070 reports tel, email or url inputs rendered RTL. R023 reports text inputs rendered LTR where Arabic input is expected.

Test editing rather than only the saved result. Type Arabic, English, leading whitespace, an emoji followed by Arabic, and a Latin campaign name followed by Arabic. Check the caret, selection, Home and End behavior, deletion around punctuation, pasted text and the direction stored on submission.

Give authors an override for ambiguous messages#

Automatic direction cannot understand intent, so a serious mixed-language editor needs a manual escape hatch. A three-state control works well:

  • Automatic for ordinary input.
  • Right to left for content whose intended base is RTL.
  • Left to right for content whose intended base is LTR.

Make the selected state visible and keyboard operable. Labels should describe text direction, not physical alignment. “Align right” is not equivalent to RTL, and an author may want an RTL paragraph with a different visual alignment.

When a user selects RTL, store rtl even if the first strong character is Latin. Do not revert to automatic direction after autosave, navigation or another editor opening the same draft. When the user switches back to Automatic, store auto rather than the result of the current guess; later edits may change the first strong character.

Browser editing controls can also change field direction. MDN notes that browsers may expose direction options or shortcuts for inputs and textareas. If your product stores direction metadata, test whether those browser actions update the dir attribute or the data your save path reads. Do not assume a toolbar state remains synchronized with a browser-level change.

Treat multi-paragraph content as multiple direction decisions#

One dir="auto" on a normal container establishes one base direction from the first strong character it finds. It does not independently classify every descendant paragraph.

Problem

html
<section class="case-summary" dir="auto">
  <p>The customer retried the upload.</p>
  <p>ما زال الملف لا يظهر في السجل.</p>
</section>

The English paragraph decides the section's automatic direction, and the Arabic paragraph inherits that LTR base.

Better

html
<section class="case-summary">
  <p dir="auto">The customer retried the upload.</p>
  <p dir="auto">ما زال الملف لا يظهر في السجل.</p>
</section>

This works when the data model truly contains two independently authored paragraphs. If the content is a rich-text document with explicit block direction, preserve each block's metadata instead of re-detecting after serialization.

textarea and pre are special: their rendering direction can be determined separately for each paragraph when automatic direction applies. Do not assume a div full of generated paragraphs behaves the same way. The distinction is documented in the MDN dir reference.

For sanitized rich text, allow only the direction values your renderer supports, preserve direction on legitimate blocks, and remove unsafe markup independently. Direction metadata does not require using untrusted innerHTML. A structured editor can store text, block type, language and direction as separate fields, then render them through known components.

Isolate dynamic strings inserted into sentences#

A message block and an inserted inline value are different jobs. dir="auto" sets the block's base direction. An inline username, reference or title also needs isolation so its internal bidi behavior does not affect the surrounding sentence.

html
<p>
  أضاف <bdi>Northwind.Support</bdi> رداً جديداً.
</p>

bdi isolates the inserted value and, without an explicit dir, determines its direction automatically. When the application already knows the value is LTR, make that contract explicit:

html
<p>
  تم ربط المحادثة بالمرجع
  <bdi dir="ltr">742-681-35</bdi>
</p>

R021 reports unisolated bidi hazards such as phones, emails and Latin tokens inside Arabic text. The bidirectional text guide covers inline ordering and punctuation in depth.

Do not put the entire translated sentence in bdi. Isolate each inserted string at its own boundary. Two adjacent dynamic values can interact with each other if they are wrapped as one unit even when each would render correctly on its own.

Style resolved direction without replacing it#

Direction is content metadata. Use HTML to declare it, then use CSS logical properties to style the resolved state.

css
.message__body {
  border-inline-start: 0.2rem solid transparent;
  padding-inline: 0.75rem;
}

.message__body:dir(rtl) {
  border-inline-start-color: #8a5cf6;
}

.message__body:dir(ltr) {
  border-inline-start-color: #2f7d66;
}

The :dir() pseudo-class selects the element according to its semantic direction, including an automatic direction resolved from its content. This makes it useful for small visual treatments that depend on the value's direction.

Do not replace the HTML attribute with direction: rtl or direction: ltr classes. In the measured Chromium behavior supplied for this guide, CSS direction changes rendering, but :dir(rtl) does not match an element whose direction exists only in CSS. The attribute is also visible to the renderer before component styles arrive.

Keep message placement separate from writing direction. A support inbox may align bubbles by author, ownership or chronology. Moving every RTL message to the right and every LTR message to the left can destroy that information. Use direction for text and logical spacing; use product semantics for the card's position.

Keep server rendering and client updates consistent#

Dynamic direction often fails during the transition between data layers rather than in the final component.

If the server renders a message with dir="auto" but the client replaces it with the viewer locale during hydration, the text can jump after load. If the server has stored metadata, both renderers should output the same explicit value. If it does not, both should keep auto.

Framework templates should bind the direction to the content record, not to global locale state:

jsx
// message comes from the support API; normalizeDirection returns ltr, rtl or auto.
<p
  className="message__body"
  dir={normalizeDirection(message.direction)}
  lang={message.language || undefined}
>
  {message.body}
</p>

Define and test normalizeDirection once at the data boundary. It should reject unexpected values and fall back to auto. Do not duplicate a direction-detection regular expression in the list row, detail view and notification preview.

Virtualized lists recycle DOM nodes. Verify that a node previously used for an RTL message does not retain dir="rtl" when it is reused for an LTR or automatic item. Stable keys help component state, but the direction attribute still needs to be updated from current data.

Portals and shadow roots follow the rendered DOM's inheritance rules. A preview dialog mounted elsewhere should receive the message direction explicitly rather than depending on the trigger's ancestors. Inspect the actual DOM when a preview looks different from the inline message.

Keep language and direction as separate fields#

lang="ar" does not set RTL, and dir="rtl" does not tell a screen reader which language to pronounce. Store and render both when the source provides them.

An Arabic sentence beginning with a Latin tag may need lang="ar" dir="rtl". An English quotation in an Arabic interface needs lang="en" dir="ltr". A technical identifier may need LTR direction but no natural-language tag at all.

Do not infer direction solely from locale. Several languages can share a script direction, content may be written in another language, and one record can contain multiple language runs. Likewise, a script detector does not resolve author intent for mixed text.

R080 reports intentional foreign-language runs missing a lang attribute. That is a language problem beside the direction problem, not evidence that one attribute can replace the other.

Build a direction test matrix#

A fixture with one Arabic sentence and one English sentence only proves the easy cases. Test values that exercise the decision boundary and the data path.

FixtureMetadataExpected base direction
Arabic sentenceautoRTL
English sentenceautoLTR
Emoji followed by ArabicautoRTL
Latin tag followed by ArabicrtlRTL
Digits-and-hyphens referenceltrLTR
Empty reply editorautoBrowser-specific empty state

Use product-shaped fixtures in the message list, conversation detail, search results, notification preview and editor. The same stored record should keep its direction in every consumer.

For browser tests, assert the attribute and the computed result:

js
// page and expect are supplied by the browser test runner.
const arabicMessage = page.locator("[data-test=arabic-message]");
const taggedMessage = page.locator("[data-test=tagged-message]");

await expect(arabicMessage).toHaveAttribute("dir", "auto");
await expect(arabicMessage).toHaveCSS("direction", "rtl");
await expect(taggedMessage).toHaveAttribute("dir", "rtl");
await expect(taggedMessage).toHaveCSS("direction", "rtl");

Then mutate the automatic message from Arabic to English without remounting it and assert that its computed direction changes to LTR. Repeat the reverse transition. This catches cached direction classes and recycled nodes.

Do a visual pass too. The accessibility tree and copied text preserve logical character order, so neither proves that punctuation or a mixed value appears correctly on screen. Inspect alignment, line wrapping, final punctuation, neutral-only values and inline inserts at narrow and wide widths.

For the editor, submit through the real path and inspect the stored payload. Reopen the draft in both Arabic and English interface locales. An explicit author choice must survive; an automatic value should still be automatic unless your data contract intentionally stores the resolved result.

Debug direction from the string outward#

When a dynamic item points the wrong way, debug the smallest authored string first:

  1. Identify whether the direction is known, stored or genuinely unknown.
  2. Inspect the string's own dir attribute, not only an ancestor's computed style.
  3. If it is auto, find the first strong directional character in the actual rendered text.
  4. Check whether a static label, hidden text or sibling was placed inside the automatic boundary.
  5. Compare the API metadata with the attribute rendered by the server and client.
  6. Inspect getComputedStyle(element).direction after live content changes.
  7. Check whether a recycled node, portal or rich-text block retained stale direction.
  8. Test visual ordering in addition to copy and screen-reader order.

Do not fix the symptom with text-align. Alignment can move the line while leaving its base direction, punctuation and run ordering wrong. Do not force the whole card to RTL because one message is Arabic either. The repair belongs on the content boundary or in the metadata that describes it.

Let intent outrank the heuristic#

Dynamic text direction is reliable when every layer preserves the same decision. The page owns its locale direction, each stored string can own explicit direction metadata, and dir="auto" handles the remaining unknown values at their smallest complete boundary.

A Ritla scan can surface Arabic text fields rendered LTR and dynamic inline values left without bidi isolation after the page renders. For the cases that need manual fixture design, continue with the bidirectional text guide and the guide to QA Arabic without speaking Arabic.

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.