BiDi debugger
Paste any text mixing left-to-right and right-to-left scripts (English + Arabic, code + Hebrew comments, quoted Persian in an English paragraph). See how the browser reorders it visually, spot the invisible bidi controls that make copy+paste flip unpredictably, and wrap the string with implicit isolates so it renders the same everywhere.
Visual render
This is exactly how the browser would render the raw string in a dir="auto" context. Switch the paragraph dir to see how the same memory-order string flips.
Bidi runs
Logical order (memory order)
Insert a bidi control
Append the chosen control character to the end of the text (or wrap the selection if you've selected a character). All are invisible — check the logical-order panel above to see them.
▶BiDi tutorial — from pure LTR to why quotes failoverview · orders of text · fixes for devs, everyday users, and designers · cursor · 10+ cases
▸What BiDi is, and why it's needed
"BiDi" is short for bidirectional text — text that mixes scripts written in different directions. Latin, Cyrillic, Greek, Han, Devanagari all read left-to-right (LTR). Hebrew, Arabic, Syriac, Aramaic, Thaana, Nʼko all read right-to-left (RTL). Ancient scripts like paleo-Hebrew and Old South Arabian are RTL too. When these mix — an Arabic quotation in an English paper, a Hebrew comment in a JavaScript file, an English brand name in a Persian tweet — the browser has to decide, character by character, which direction each glyph reads and where the cursor should sit.
A quick vocabulary note before we go further: programmers call any run of text characters a string— the letters, digits, spaces, and punctuation that make up a word, a sentence, or a whole paragraph, stored side by side in memory. Everything you type into the box above is a string. Everything on this page is strings. When we say "the algorithm runs over a string" below, that just means it reads the characters left-to-right (in memory) and decides what to do with each one.
The rules live in Unicode Annex #9: The Bidirectional Algorithm (UAX #9). It runs over every string that mixes directions and produces a resolved level for each character (an even integer = LTR, odd = RTL). Then the renderer walks the string in visual order using those levels. The algorithm is deterministic, spec'd since Unicode 3.0 (1999), and implemented consistently in every browser, terminal, OS, and Word processor.
The reason it's hard: Unicode strings are stored in logical order (the order you'd dictate the letters), but they render in visual order (what you see on screen). For pure LTR or pure RTL text these are trivially related — one is the reverse of the other. For mixed text they diverge in ways that are locally correct but globally surprising: a comma you typed at the end of an Arabic word can appear at the visual beginning of it; a closing quote can migrate two words to the left; a cursor arrow-key that feels "right" can move you further from where you meant to edit.
The tool above shows you the algorithm's decisions step-by-step for any string. The tutorial below shows the common failure modes, in escalating order.
▸ Three orders of text: typing, logical, visual
People often ask "is typing order the same as logical order?" The short answer: typing order = logical order in modern text-input systems. Visual order is the one that differs from both. Three definitions, then the edge cases where the equality breaks.
- Typing order
- The sequence of keys you press. Each keystroke appends its character to the buffer at the current cursor position.
- Logical order (memory order)
- How the string is stored in memory / on disk / in the DOM — a linear sequence of Unicode code points, ordered as they were appended.
- Visual order (reading order)
- How pixels land on screen after UAX #9 runs. LTR runs stay in memory order; RTL runs are reversed; mixed text is chunked and each chunk reversed independently.
Take "שלום". You press the four keys ש · ל · ו · ם in that order. Memory holds [ש, ל, ו, ם] — same order. The renderer walks it in visual order and paints ם ו ל ש, i.e. right-to-left on the page. Typing order and logical order are identical; visual order is the reverse.
For LTR text (English, code) all three collapse to the same order — which is why LTR-native developers spend years not noticing the distinction exists.
Where the equality between typing and logical DOES break down — a handful of edge cases:
- Vietnamese-style IME composition. You may press
ethen a tone key then a hat key; some IMEs storeU+1EBF(precomposed), others storee + ̂ + ́(decomposed). Typing sequence ≠ stored sequence. - Unicode normalization on save. Editors that apply NFC or NFD reorder combining marks into canonical order. Typed
e + ́ + ̂may end up stored ase + ̂ + ́(or asếentirely). - Old "visual-order" Hebrew. Pre-Unicode Hebrew systems (VT100 terminals, some DOS files) stored text in visual order — you typed right-to-left BUT saved letters in the order they appeared on screen. Modern Unicode uses logical order universally, but you occasionally encounter legacy visual-order files that render as gibberish until re-reversed.
- Autocomplete and paste.These insert whole strings at once — no per-keystroke typing sequence exists. "Typing order" is undefined here; only logical order is meaningful.
Practical consequence: cursor keys move by LOGICAL order, not visual.Press right-arrow in a Hebrew word and the cursor moves to the next logical character — which sits visually to the LEFT. This is spec-defined and consistent, but it feels backwards to LTR-native users. macOS and Windows both offer an "RTL cursor keys" option that swaps left/right in RTL contexts; enable it if the logical behavior bothers you.
▸ For developers — where to add BiDi controls in HTML / CSS / plain text
Most bidi bugs come from either doing nothing when you should isolate (the failure modes in cases 4-5, 9) or reaching for legacy overrides when isolates would do (the RLO attack in case 9 is possible because LRO/RLO/PDF even exist). Here is the concrete decision tree.
Add an isolate WHEN:
- You're displaying user-supplied textwhose direction you don't control — usernames, comments, product titles, chat messages, email subjects. Every one of these gets wrapped, always, no exceptions. This is the single biggest ROI move.
- You're embedding a proper noun or quoted phrase in the opposite direction — an Arabic name in an English paragraph, an English brand in a Persian tweet. Wrap the embedded chunk even if quotes seem to work today (case #5 shows how one added punctuation mark breaks it).
- Two adjacent runs of different directions could bleed neutral punctuation into each other — parentheticals, colons, dashes, ellipses. Any neutral character between strong-typed chars of different directions is a bidi bug waiting to happen.
- Content moves across systems(paste-to-email, export-to-PDF, database-to-page). The receiver's paragraph direction may differ from yours; isolates make the run render identically regardless.
- You render RTL content inside a fixed-LTR UI (Slack in English locale, most corporate wikis, code editors). The UI direction anchors resolution; everything RTL you display should be isolated.
- Filenames, URLs, identifiers, code strings — any place where a wrong-side punctuation change means the wrong thing gets clicked, run, or referenced.
SKIP the isolate when:
- The text is entirely one direction. Pure English, pure Arabic, pure Hebrew — no isolation needed, wrapping adds no-op invisible characters and clutter to your markup.
- You are the author and you've verified the render in the actual target contexts.If it looks correct in email, chat, printout, and screen reader, don't add controls prophylactically. But re-check after any edit — case #5 is a one-character-away failure.
- Inside a code compiler / interpreter. Parsers read logical order, so
"שלום"is the same string whether isolated or not — the compiler doesn't care. But your COMMENTS and STRING LITERALS displayed to humans still do.
WHERE to add the isolate — three surfaces, same effect:
<bdi> (old CMS, design-system spans).Which isolate character? A quick chooser:
- FSI (U+2068) → default choice.The isolate infers direction from the first strong-typed character inside. Right for 95% of cases including all "wrap this user-supplied thing" scenarios.
- LRI (U+2066) when you know for certain the content is LTR (an English brand name). RLI (U+2067)when you know it's RTL. Use these only if the FSI first-strong heuristic guesses wrong for your content.
- PDI (U+2069) always closes. One PDI per opening isolate, in matching nesting. Unclosed isolates cascade until the paragraph ends.
- LRE / RLE / LRO / RLO / PDF are legacy.Don't add them to new content. They stay in Unicode for backward compat, but isolates supersede them and never have the RLO-attack failure mode.
Copy-paste rule for React / Vue / Svelte:
Anywhere you render user text — {userName}, {comment.body}, {article.title} — wrap it:
<bdi>{userName}</bdi>That single change eliminates ~90% of user-facing bidi bugs in the average app. The other 10% are cases like the code comments above, where you need to hand-wrap around whole segments you authored yourself.
▸ For everyone — how to fix bidi in Word, Google Docs, Gmail, iMessage, WhatsApp
Most everyday apps have a "paragraph direction" button or shortcut that flips a block of text between LTR and RTL. If your period lands on the wrong side, or your Hebrew/Arabic sentence renders with punctuation reversed, the fix is usually one click.
Microsoft Word (all platforms)
- Toolbar: Home tab → look for two paragraph icons with LTR / RTL arrows. Click the RTL one for the current paragraph.
- Keyboard: Ctrl + Right-Shift (Windows) or Cmd + Ctrl + Right-Shift (macOS) → RTL paragraph. Ctrl + Left-Shift / Cmd + Ctrl + Left-Shift → LTR paragraph.
- If the RTL buttons don't appear, go to File → Options → Language and enable a right-to-left editing language (Hebrew, Arabic, Persian, Urdu). Word hides the RTL controls until it knows you need them.
Google Docs
- Menu: File → Language → pick Hebrew / Arabic / Persian / etc. That enables RTL paragraph controls in the toolbar.
- Toolbar: paragraph-direction buttons appear next to the alignment buttons (the ones with numbered lines). Click the RTL one.
- Keyboard: Ctrl/Cmd + Shift + E, then P to open the paragraph settings, or just click the toolbar buttons directly.
Apple Pages / Numbers / Keynote
- Menu: Format → More → Writing Direction → Right to Left. Applies to the selected paragraph or text box.
- System-wide RTL cursor keys: System Settings → Keyboard → Text Input → Advanced→ check "Move cursor visually in RTL text."
Gmail / Outlook
- Gmail: Settings (gear) → See all settings → Generaltab → "Right-to-Left editing support" → "Enable." A tiny direction-toggle button appears in the compose toolbar after that.
- Outlook desktop: Same paragraph shortcuts as Word — Ctrl + Right-Shift and Ctrl + Left-Shift. Outlook Web uses the same RTL button as Gmail (once enabled in settings).
iMessage / WhatsApp / Slack / Telegram / Discord
- Message text is normally
dir="auto"— the app infers direction from the first strong-typed character. Start a message with Hebrew or Arabic → the whole bubble renders RTL, punctuation lands correctly. Start with English → LTR. So the fix is often "type the RTL word first." - If a message mixes languages and renders wrong, iOS and Android both support long-press → Select All → change paragraph directionin most text fields. Look for a "paragraph direction" or arrow icon in the context menu.
- Slack has no per-message direction toggle. Content is always dir="auto"; if your message renders wrong, reorder so the first word is in the intended direction, or wrap with FSI…PDI from the plain-text section below.
Browsers (Chrome, Safari, Firefox)
- Most form fields on the web are
dir="ltr"by default (the classic period-jump cause). You can't change this without editing the site's HTML. - Workaround in any text field: type an RLM (U+200F) at the very start of your text. On Mac, System Settings → Keyboard → Text Input → Show Input menu, then Input menu → Show Character Viewer → search "RLM" → double-click to insert. On Windows, Alt+X after typing
200Fin supported fields. - Or install a browser extension like "Force RTL" that adds a direction toggle to every text field.
▸For designers & artists — Photoshop, Illustrator, InDesign, Figma, Canva
Design and print tools have a specific gotcha that Word and browsers don't: many editions of Adobe apps ship WITHOUT the Middle Eastern text engine. Arabic looks broken (letters disconnected, no joining), Hebrew punctuation lands wrong, kashida stretching doesn't work. The fix is either switching engines or installing a different edition.
Adobe Photoshop, Illustrator, InDesign (all recent versions)
Adobe ships these in two flavors: the North American / International English edition (default when you install from Creative Cloud) and the Middle East & North Africa edition. Only the MENA edition ships the Middle Eastern text composer that handles Arabic joining, Hebrew punctuation, RTL paragraphs, and kashida.
Switch editions (recommended):
- Creative Cloud desktop app → click your profile picture → Preferences → Apps → Default install language → change to English (Arabic) or English (Hebrew). Then reinstall Photoshop / Illustrator / InDesign — the new install includes the Middle Eastern composer.
- Your files stay compatible with the standard edition; you get all the same tools PLUS the MENA features.
Enable RTL in the MENA edition (per document):
- Character panel: language dropdown must be set to Arabic / Hebrew / Persian for the text to shape correctly. Even in the MENA edition, if language is "English" the Arabic letters render as isolated forms.
- Paragraph panel: use the "Middle Eastern & South Asian Every-Line Composer" (or Single Line variant). Set paragraph direction to Right-to-Left via the RTL alignment icon.
- Kashida justification: only appears in the Paragraph panel dropdown when Middle Eastern composer is active. Choose "Kashida" for justification style to enable the classical Arabic wide-letter-stretching.
Standard-edition workaround:
If you can't reinstall, enable the "World-Ready Composer" via a hidden setting. In Photoshop / Illustrator: Preferences → Type→ check "Middle Eastern Text Engine" (or "Show Indic Options" depending on version). Restart the app. This adds partial MENA support to the standard edition, though some features (like Kashida) still require the full MENA install.
Figma
- Figma respects the browser's bidi algorithm, so Arabic and Hebrew shaping and joining WORK out of the box in a Figma text layer. Kashida does NOT — Figma doesn't expose the OpenType kashida GSUB feature.
- Paragraph direction: Text panel → three dots (…) → Text direction → Right-to-Left. This is per-text-layer.
- Common gotcha: pasted Arabic from a browser sometimes arrives with LTR paragraph direction embedded. If your Arabic renders punctuation-flipped, apply Text direction → RTL manually.
Canva
- RTL support is limited. Basic Arabic and Hebrew text render with correct joining/shaping, but there's no per-text-box RTL paragraph toggle. Punctuation often ends up on the wrong side.
- Workaround: paste a Unicode RLM character (U+200F) at the start of your text. The easiest way is to copy it from Compart's Unicode reference and paste at the start of your text box.
Print / prepress specifics
- PDF export: as long as your source app rendered the Arabic / Hebrew correctly (all joining, punctuation in the right place), the PDF preserves the visual layout — bidi resolution has already happened before export. Print reproduces exactly what you see.
- Font selection matters.Not every font supports Arabic joining or Hebrew punctuation positioning. Adobe Arabic, Adobe Hebrew, SST Arabic, and any Noto Sans/ Serif Arabic or Hebrew variant are safe defaults. A font labeled "Arabic" but without the positional shaping rules will render letters disconnected regardless of your app's bidi settings.
- Kashida in print: only Adobe MENA edition + Middle Eastern composer will apply kashida justification. If you need scribal-style wide letters in a design, either use those tools, or use the Semitic Stretch fonts from /fonts which bake widened letter variants into the font itself (works in any app that renders the font).
▸ Fixing the three canonical failures
The overview mentioned three specific breakage patterns. Each has a concrete fix. Copy any pair into the analyzer above and switch paragraph direction to see the difference.
Failure #1: A comma typed at the end of an Arabic word appears at the visual beginning of it.
Memory: The word مرحبا, was surprising. — you typed the comma after the Arabic. In an LTR paragraph the comma is a neutral flanked by AL on the left and was(L) on the right. UAX #9 rule N1 resolves it toward the strongest adjacent (AL wins because "was" is separated by a space). The comma joins the RTL run and appears visually LEFT of the Arabic letters — same paragraph reads as The word ,مرحبا was surprising.
Fix:
The word مرحبا, was surprising.
FSI…PDI seals the Arabic. Now the comma sits outside the isolate, in the LTR paragraph run, and resolves as LTR — staying visually AFTER the Arabic word. In HTML: The word <bdi>مرحبا</bdi>, was surprising.
Failure #2: A closing quote migrates two words to the left.
Memory: He said "مرحبا يا صديقي" and waved. — quoted Arabic phrase inside an English sentence. The closing " is a neutral. Its neighbors are Arabic on the left, space then and (L) on the right. AL wins; quote absorbs into the RTL run and slides across it visually until it exits the RTL region — landing on the far LEFT of the phrase, next to the opening quote. Reads as He said ""مرحبا يا صديقي and waved — both quotes on the same side.
Fix:
He said "مرحبا يا صديقي" and waved.
Wrap the ENTIRE quoted phrase — including both quotes — inside FSI…PDI. Inside the isolate, all neutrals resolve within the sealed context; outside, the surrounding English runs untouched. Both quotes end up on the correct sides of the Arabic phrase. Same fix in HTML: He said <bdi>"مرحبا يا صديقي"</bdi> and waved.
Failure #3: A cursor arrow-key that feels "right" moves you further from where you meant to edit.
Memory: The word مرحبا means hello.— cursor sits between the last Arabic letter and the following space. You press → (right-arrow) expecting to move "forward" in the sentence. But right-arrow moves by logical order, one code point forward in memory. In this position, the next code point is the space — which visually sits between the Arabic word and means. So you moved from the RIGHT edge of the Arabic (visually) into the space that comes AFTER the Arabic visually — which is to the RIGHT. Feels correct.
But if the cursor sits BEFORE the first Arabic letter (at the boundary between the opening space and مـ), right-arrow moves logically forward — onto the first Arabic letter — which visually sits at the RIGHT edge of the Arabic word. So cursor jumps a whole word to the RIGHT visually.
Fix:
There isn't a per-string fix; this is a UX-level choice by the OS/editor. Three options:
- Enable "RTL cursor" mode— macOS: System Settings → Keyboard → Text Input → "Move cursor visually in RTL text." Windows: same option in the Language settings. In this mode arrow keys move by VISUAL order regardless of paragraph direction. Right- arrow always moves right on screen.
- Use Home / End for edge-of-line navigation instead of many arrow presses — these are direction-invariant.
- For programmatic cursor placement (test tools, IDE plugins) — always calculate positions from character indices in the STRING, not from screen coordinates. The mental model must be logical order.
▸"I typed a period and the cursor jumped" — the most common bidi complaint
You type a whole Hebrew or Arabic sentence, everything looks right, and then you press . or ,at the end — the period appears on the WRONG side of the sentence and the cursor teleports across the screen. This is the single most common bidi complaint from native RTL typists. Here's exactly what's happening and how to stop it.
The scenario
Text box has default paragraph direction dir="ltr" (or unspecified, which is the same on most Latin-locale systems). User types the Hebrew sentence שלום עולם("Hello world"). Renders visually as שלום עולם — Hebrew reversed, RTL run flowing right-to-left. Cursor sits at the left edge of the visual text (which is where memory-position-end lands in an LTR paragraph).
User presses .. Memory becomes [ש, ל, ו, ם, , ע, ו, ל, ם, .]. Renders as שלום עולם. — the period appears on the RIGHT of the sentence, and the cursor jumps from the left edge (where it was) to the far right (after the period). Feels catastrophic.
Why the period lands on the wrong side
The period is a neutral (ON). Its neighbors: ם (R) on one side, end-of-paragraph on the other. UAX #9 rule N2 says: neutrals adjacent to the paragraph boundary resolve toward the paragraph direction. Paragraph is LTR here, so the period resolves as L (level 0). The Hebrew run is at level 1 (odd = RTL). Level-0 chars come AFTER level-1 chars visually in an LTR paragraph → period sits to the right of the Hebrew.
From a Hebrew reader's perspective, the sentence "ends" on the LEFT (where reading order stops). Putting the period on the RIGHT is like putting an English period at the beginning of a sentence: .Hello world. Nonsensical.
Fixes, in order of preference
1. Set the container direction to RTL (best)
<input dir="rtl" ...> <textarea dir="rtl" ...> <div dir="rtl">...</div>
Now the paragraph is RTL. When the user types a period at the end, UAX #9 rule N2 resolves it toward RTL (the paragraph direction), giving it level 1 — SAME as the Hebrew — so the period sits at the visual LEFT of the sentence, correctly placed as sentence-end for RTL reading. Cursor stays where the user expects.
2. Use dir="auto" if the field accepts both directions
<input dir="auto" ...> <textarea dir="auto" ...>
Browser infers paragraph direction from the FIRST strong-typed character in the field. Empty field → defaults to the app's locale. Type an English word first → LTR. Type Hebrew or Arabic first → flips to RTL. This is the RIGHT DEFAULT for any user-facing text input in a multilingual app. It costs nothing and prevents the period-jump for every RTL typist.
3. Insert RLM after the punctuation (targeted fix)
שלום עולם. (period followed by U+200F RLM)
If you can't change the container direction (legacy CMS, third-party embed), give the period a strong RTL neighbor. RLM (Right-to-Left Mark, U+200F) is an invisible strong-R character; placing it after the period gives the neutral a strong-R neighbor to its right, so bidi resolution treats the period as RTL and it lands on the visual left. This is the fix documented in W3C's "Inline markup and bidirectional text in HTML".
4. Wrap the whole sentence in RLI…PDI
שלום עולם. (RLI + text + PDI)
Force an RTL isolate around the sentence with U+2067 RLI (Right-to-Left Isolate) and U+2069PDI. Inside the isolate the "paragraph direction" is RTL regardless of the surrounding container. Use this when you're emitting the string into some other person's paragraph (email body, chat message, log line) and can't predict their container direction. FSI would work too, but RLI is more explicit for content you KNOW is RTL.
Recommendation for app developers
Every user-facing text input should be dir="auto"— one attribute, covers the entire class of "period jumped to the wrong side" complaints for every RTL user. Same for chat message bubbles, comment rendering, email preview, anywhere user content might be RTL. It costs nothing. LTR users see no change (their first char is Latin → paragraph stays LTR).
<input type="text" dir="auto" />
<textarea dir="auto" />
<div dir="auto">{userMessage}</div>▸ How the cursor moves in bidi text
Cursor movement in bidi text is one of the least-intuitive parts of the algorithm, and the source of a huge share of user complaints. Here is the rule and the reasoning.
The rule: arrow keys move by logical order by default. → advances the cursor to the next code point in memory. ← moves to the previous. This is true regardless of which direction that character is visually rendered.
Why logical, not visual?Because "next character" in a mixed-direction string is ambiguous visually. Consider The word مرحبا means hello. If the cursor sits between the dof "word" and the space before Arabic, what should → do?
- Logical answer: move onto the space (memory index +1). Visually the cursor jumps to sit before the LAST Arabic letter (rightmost), because that's where the Arabic word's first-memory-position renders.
- Visual answer: move one pixel to the right. But the next thing to the right IS the space, which is between "word" and the Arabic word visually and logically. So visual and logical happen to agree here.
But now cursor is inside the Arabic word's space boundary. Press → again. Logically it moves to م (first memory position of Arabic). Visually م sits on the RIGHT edge of the word — so the cursor visually jumps across the whole Arabic word to the right. Visual answer would have moved it LEFT (into the last visually-rendered Arabic letter, which is the LAST-memory letter). Logical and visual now disagree.
Design tradeoff:Unicode's original choice was logical because it's predictable— cursor position = memory index, no ambiguity. But it's unintuitiveat bidi boundaries. Windows shipped a "visual arrow keys" toggle in NT 4.0 (1996) and macOS added one in 10.3. Both are OFF by default; power users of Arabic/Hebrew often turn them on.
Two more cursor gotchas worth knowing:
- Home / End are direction-invariant.Home goes to logical START (visual left in LTR paragraph, visual right in RTL paragraph). End goes to logical end. These usually do what you want because "beginning of the line" and "end of the line" are direction-relative concepts.
- Backspace deletes the LOGICALLY previous char. That may be visually to the left OR right of the cursor depending on what you just typed. In Hebrew or Arabic context, backspace visually deletes to the LEFT (which is "forward" in RTL reading), not to the right as LTR users expect.
For programmers: DOM APIs (selectionStart, textRange, substring) all work in logical order. A cursor at index 5 is always the same memory position, regardless of what pixel it's rendered at. If you compute cursor positions from mouse clicks (e.g., a canvas-based text editor), you have to call document.caretPositionFromPoint()or the equivalent — never assume "X pixels from left" = "X characters into the string."
Each case below shows one specific bidi phenomenon. Click the Load button on any case to push its text into the analyzer above and see the runs, logical order, and per-character inspector all update.
1. Pure LTR — the boring baseline
Every character is Bidi_Class=L. Nothing to resolve; memory order and visual order match. Cursor moves left-to-right. Useful only as a sanity anchor before adding complexity.
2. Pure RTL — Hebrew or Arabic on its own
Every character is R (Hebrew) or AL (Arabic). The browser reverses visual order so the first-typed character sits at the right edge. Cursor moves right-to-left. Still simple — one script, one direction, no ambiguity.
3. First mix — one RTL word inside an LTR paragraph
Bidi has to decide where the boundary between the English and Arabic runs falls. The strong-typed chars carry their own direction (L for English letters, AL for Arabic). The spaces around مرحبا are neutrals (WS) that inherit from their strong neighbors. Result: Arabic renders as a discrete RTL island, everything else stays LTR. Reads correctly by luck — the neighbors happen to give the neutrals unambiguous direction.
4. Why quotes seem to work — neutrals absorbed both sides
You'll notice quoting an inserted word almost always looks right. The reason is subtle and NOT what people think. Quotes are ON(Other Neutral) — they don't have direction of their own. Because both quotes sit between the surrounding English (L) and the Arabic (AL), each quote gets a strong neighbor on one side and a neutral (space) on the other. UAX #9 resolves them toward the paragraph direction (LTR here), so they render "مرحبا"in visual order — the quotes stay on the "outside" of the Arabic word. This is resolution by neighborhood, not real isolation. It happens to look right for one word in a simple sentence, and that's why the trick is so common. It fails as soon as complexity grows (see below).
5. Where quotes fail — RTL run absorbs trailing punctuation
Add a ! right after the closing quote. That ! is a neutral. The strong-typed char to its left is Arabic (AL). Bidi resolves the ! against its strongest neighbor, which is the Arabic — so ! and the closing " get pulled visually INTO the RTL run, ending up before the word in reading order. The line reads as though someone wrote "!" مرحبا surprised me. Not what the author typed. Neutrals-follow-neighbors is fragile.
6. The real fix — Unicode isolates (FSI…PDI)
Wrap the inserted phrase in FSI (U+2068, First Strong Isolate) and PDI (U+2069, Pop Directional Isolate). Same text, same characters, but now the run "مرحبا"! is a sealedbidi context — UAX #9 resolves it in isolation, and the surrounding English can't pull its punctuation in. The isolates are invisible, so you don't see them, but they do all the work. This is what the "Wrap for safe paste" button up top produces. Bonus: the position of the ! inside the quotes is now controlled by MEMORY ORDER, not by resolution heuristics. Type "!مرحبا" (bang FIRST) → the bang appears visually to the LEFT of the Arabic word; type "مرحبا!" (bang LAST) → the bang appears visually to the RIGHT. Both are valid, both are stable, choose by semantics. The isolate just guarantees your choice sticks.
6b. Choosing bang position — same content, two intents
Two isolated variants. Both have exactly the same characters (opening quote, bang, Arabic, closing quote) — but the bang appears in a different memoryposition. Because the content is isolated, memory order fully determines visual position and the surrounding English can't interfere. Rule of thumb: type the punctuation where you want it RELATIVE TO THE ARABIC WORD, in reading order. In the top line, bang comes before the word in memory → visually to the LEFT of the Arabic. In the bottom, bang comes after → visually to the RIGHT. This is the whole reason isolates matter: they make position deterministic.
7. Same fix in HTML — the <bdi> element
In web content you can wrap the phrase in <bdi>…</bdi> instead of adding invisible Unicode chars. The <bdi> element (Bi-Directional Isolate) is spec-defined to apply unicode-bidi: isolate and direction: auto. Same result as FSI…PDI, cleaner in source, and grep-friendly. If you don't control the markup, the Unicode isolates are the fallback. The sample text here shows the HTML — the visual render will render it as literal tags, not interpret them.
8. CSS-only variant — unicode-bidi: isolate on any span
If you can't use <bdi> (some old CMS, custom component), any element with unicode-bidi: isolate; direction: auto gives you the same behavior. bidi-override is the stronger sibling — it forces every character to a single direction regardless of its own class, useful for filename-safe display, but breaks natural Arabic/Hebrew reading order.
9. The RLO attack — same primitives, weaponized
U+202E is RLO (Right-to-Left Override) — it forces every following character to be RTL until a PDF. Attackers put it inside filenames so invoice<RLO>gnp.exe displays as invoice.exe.png-looking text, tricking users into double-clicking an executable. Modern OSes and mail clients now strip or warn on stray overrides for this reason. Load this example and look at the Bidi runs panel — the RLO shows up highlighted in fuchsia in the logical-order grid.
10. Numbers, the sneakiest neutrals
Digits have their own bidi classes: European (0-9) → EN, Arabic-Indic (٠-٩) → AN. These are weak — they render left-to-right internally, but their surroundings pull them around. In an Arabic paragraph, the Latin digits 2025stay LTR internally but appear on the "wrong" side of the slash. Mixing digit systems in a currency amount is the classic real-world case where copy-paste breaks silently.
Summary — how to make bidi predictable
- Quotes are not isolation.They're neutrals that happen to resolve nicely in the simplest cases. Don't rely on them once a sentence has multiple bidi transitions, trailing punctuation, or user-supplied text.
- Isolate with intent. Wrap every user-supplied name, quoted phrase, or bidi-unknown chunk in
<bdi>(HTML), FSI…PDI (Unicode), or an element withunicode-bidi: isolate(CSS). This is the same thing three ways. - Never store legacy overrides. Avoid LRE/RLE/LRO/RLO/PDF in persisted content — they cascade badly across paste boundaries and enable the RLO attack. Use isolates.