Right-to-Left Text
Bidirectional text is where an editor that fakes its own selection either works or falls over. Selection here is resolved by measuring with Ranges — the document is never rewritten to find out where a character is — so the interesting question is not "does Arabic render" — the browser does that — but whether selection, the caret and arrow keys follow the visual order when the visual order disagrees with the logical order.
Try it. Click into the Arabic paragraph, walk the caret with the arrow keys, and double-click a word in each of the mixed lines.
<tosijs-styled-editor widgets="default">
<h2>عربي — a right-to-left block</h2>
<p dir="rtl">
هذا النص مكتوب من اليمين إلى اليسار. حاول تحديد كلمة بالنقر المزدوج،
ثم حرّك المؤشر بمفاتيح الأسهم.
</p>
<h2>עברית — another right-to-left block</h2>
<p dir="rtl">
זהו טקסט מימין לשמאל. נסו לבחור מילה בלחיצה כפולה ולהזיז את הסמן.
</p>
<h2>Mixed, in a left-to-right paragraph</h2>
<p>
An English sentence containing العربية in the middle, then back to English.
Numbers embed too: the price is ١٢٣٤ dinars, or 1234 if you prefer.
</p>
<h2>Right-to-left block with embedded left-to-right</h2>
<p dir="rtl">
جملة عربية تحتوي على English words في المنتصف، ثم تعود إلى العربية.
</p>
<p dir="rtl">
الأمر هو <code>bun run make</code> وعنوان الموقع
<code>https://tosijs.net</code> — كلاهما يُعرض من اليسار إلى اليمين
داخل فقرة من اليمين إلى اليسار.
</p>
<p dir="rtl">
الإصدار 1.13.0 صدر في 2026، والسعر 1,234.56 — الأرقام والترقيم
هي الحالة الأصعب لأن اتجاهها يتغير داخل الجملة.
</p>
<ul dir="rtl">
<li>عنصر عربي عادي</li>
<li>عنصر يحتوي على <code>setBlockType h1</code> في وسطه</li>
</ul>
<h2>A list mixing both</h2>
<ul>
<li>Left-to-right item</li>
<li dir="rtl">عنصر من اليمين إلى اليسار</li>
<li>Another English item with עברית inside it</li>
</ul>
</tosijs-styled-editor>
What to look for
| Behaviour | Why it is hard |
|---|---|
| Double-click a word in an RTL run | Word boundaries come from Unicode segmentation of the block's text, which is in logical order while the rendering is visual |
| Arrow keys across a direction boundary | One logical step can move the caret to the other end of a run |
| Caret position at the seam between scripts | The seam has two valid positions — the end of the LTR run and the start of the RTL run — that paint at the same place |
| Selecting across a boundary | A logically contiguous range is visually discontiguous, so a single highlight rectangle is wrong |
| An LTR run inside an RTL line | Code, URLs and version numbers stay left-to-right inside a right-to-left sentence, so one line can change direction twice |
| Numbers and punctuation | Digits are left-to-right even in RTL text, and trailing punctuation resolves against the surrounding run, not the number |
The same text in contentEditable
Here is the identical content in a plain contentEditable, so you can put the two
side by side and judge for yourself. This is the browser's own selection, caret and
bidi handling — the thing this component replaces.
<div
contenteditable="true"
spellcheck="false"
style="border: 1px solid var(--tosi-border-color, #ccc); border-radius: 4px; padding: 8px 12px; min-height: 200px; max-height: 400px; overflow-y: auto; background: var(--tosi-background, #fff); color: var(--tosi-text, #222)"
>
<h2>عربي — a right-to-left block</h2>
<p dir="rtl">
هذا النص مكتوب من اليمين إلى اليسار. حاول تحديد كلمة بالنقر المزدوج،
ثم حرّك المؤشر بمفاتيح الأسهم.
</p>
<h2>עברית — another right-to-left block</h2>
<p dir="rtl">
זהו טקסט מימין לשמאל. נסו לבחור מילה בלחיצה כפולה ולהזיז את הסמן.
</p>
<h2>Mixed, in a left-to-right paragraph</h2>
<p>
An English sentence containing العربية in the middle, then back to English.
Numbers embed too: the price is ١٢٣٤ dinars, or 1234 if you prefer.
</p>
<h2>Right-to-left block with embedded left-to-right</h2>
<p dir="rtl">
جملة عربية تحتوي على English words في المنتصف، ثم تعود إلى العربية.
</p>
<p dir="rtl">
الأمر هو <code>bun run make</code> وعنوان الموقع
<code>https://tosijs.net</code> — كلاهما يُعرض من اليسار إلى اليمين
داخل فقرة من اليمين إلى اليسار.
</p>
<p dir="rtl">
الإصدار 1.13.0 صدر في 2026، والسعر 1,234.56 — الأرقام والترقيم
هي الحالة الأصعب لأن اتجاهها يتغير داخل الجملة.
</p>
</div>
Worth trying in both, in RTL text specifically: click between two letters of a
cursive cluster, drag a selection across a direction boundary, and walk the caret
through an embedded bun run make with the arrow keys.
One measured comparison, on click-to-caret accuracy — probing every non-space character in a paragraph and asking which character the caret landed beside:
contentEditable (caretPositionFromPoint) |
|
|---|---|
| Hebrew | 100% |
| Arabic | 98.8% |
| LTR with embedded Arabic | 98.4% |
| RTL with embedded LTR | 98.1% |
So the browser's own hit-testing is not exact either. Cursive clusters are the common residue: where two letters render as one glyph, their rects overlap and a point inside genuinely belongs to both.
<code>, <kbd> and <samp> are given direction: ltr; unicode-bidi: isolate
inside the document, because they are left-to-right by nature: without the
isolate, a URL's slashes or a trailing period resolve against the paragraph's
RTL base direction and jump to the wrong end, even though the letters between
them render correctly. An explicit dir on the element opts out.
None of this is faked with contentEditable's help — there is no browser
selection to lean on — so this page is the honest test of how far the DOM-only
approach gets. Anything broken here is a real bug worth filing rather than a
known limitation of the approach.
Typing across a direction boundary
Type Latin into one of the Arabic paragraphs. The run and the caret are wrapped
in a <span dir="ltr"> isolate as you type, and consecutive characters extend
that one isolate rather than making a new one each keystroke.
That is not cosmetic. The caret is an ELEMENT, and bidi treats an empty inline
as a neutral — so without the isolate it resolves against the block's base
direction rather than the run being typed, and jumps to the far side of the
line while your text appears somewhere else. Isolating the run fixes the caret
and renders the run correctly, which is the same fix <code> needs.
Setting direction
dir is ordinary markup and is preserved through editing, so a document can mix
directions per block:
<p dir="rtl">…</p>
There is no toolbar command for it yet. setBlocks only writes CSS properties,
and direction set in CSS does not survive serialization the way the attribute
does.