When not to use bidi control characters
Learn when to replace invisible bidi controls with HTML, how to protect stored values and copy actions, and which plain-text exceptions justify controls.
By Omran Khleifat, Founder of Ritla
Guide14 min read
A clip reference looks correct in an Arabic media library, but the value copied by its “Copy reference” action no longer matches the reference accepted by the lookup service. The display fix added invisible directional characters to the same string the copy action uses. One component now looks better while another workflow receives different data.
Do not add bidi controls to a shared value when the actual requirement belongs to one HTML presentation. Keep the reference intact and express direction on its element. Reserve character-based formatting for destinations that need a plain-text string and cannot carry the required inline markup.
That is a choice about boundaries, not a ban on invisible characters. A locale formatter can intentionally return directional marks, and a plain-text notification can need an isolate. The engineering mistake is letting a local display workaround become an undocumented property of a filename, identifier, translation or reusable content field.
Do not decorate the value used for identity#
The media library uses the clip reference 63081752-QZ as an exact lookup key. A developer wraps it in LEFT-TO-RIGHT ISOLATE and POP DIRECTIONAL ISOLATE before passing it to the detail component.
This small reproduction makes the data change explicit:
const clipRef = "63081752-QZ";const decoratedRef = `\u2066${clipRef}\u2069`;const knownRefs = new Set([clipRef]); console.log(knownRefs.has(clipRef)); // trueconsole.log(knownRefs.has(decoratedRef)); // falseThe two strings contain different characters. This exact-match example does not imply that every search system handles controls identically. It shows why the frontend must not assume that presentation characters are harmless to every consumer of an identifier.
The risky implementation can be as small as assigning decoratedRef back to a record's reference field. Once that happens, route generation, validation, a copy action and an export may all receive the decorated value. Each downstream consumer would need to understand an operation that was introduced to fix one screen.
Better
<p dir="rtl">مرجع المقطع: <span dir="ltr">63081752-QZ</span></p>The element owns the direction boundary. Its text remains the original reference, so a component can use the same raw value for lookup and copying without first undoing formatting.
This is the general recommendation for HTML: use dedicated markup where it is available. W3C explains the structural reasons in CSS vs. markup for bidi support. Adding invisible characters is unnecessary when an element can carry the requirement directly.
There is a limit to the recommendation. A reference received from an external system might already contain controls as part of that system's documented representation. Establish the input contract and preserve evidence before changing it. Do not silently rewrite external keys because the local product would have designed them differently.
Do not repair an HTML boundary inside a string#
Before inserting a control, identify exactly what is wrong on screen. In the clip-reference example, the reader expects the reference's recorded group order, independently of the Arabic label's reading direction. That is an inserted-value boundary.
A plain wrapper does not establish it:
<p dir="rtl">مرجع المقطع: <span>63081752-QZ</span></p>The value starts with digits and ends with Latin letters, a shape that can reorder after Arabic text. The brief's concrete browser rules were measured in Chromium 151 on 2026-09-24. Verify the component in the Firefox and Safari versions your product supports rather than treating those measurements as evidence for every engine.
The preceding span dir="ltr" expresses the known reference format. For a user-entered clip title whose direction is unknown, the suitable default is different:
<p dir="rtl">عنوان المقطع: <bdi>ضوء الصباح فوق الميناء</bdi></p>An existing link or heading can take dir="auto" instead of gaining another wrapper. W3C's inline bidi guidance explains how to isolate inserted phrases while retaining their meaningful elements.
Put only the inserted value inside that boundary. If the Arabic label is included in an automatic-direction element, the label can select RTL for the whole combined string. A control around that same oversized range would preserve the wrong scope rather than fix it.
Do not infer direction from the interface locale for arbitrary titles. A Latin title can appear in an Arabic interface and an Arabic title in an English one. Use explicit direction for known formats and automatic direction for genuinely unknown content. Neither choice requires modifying the underlying title.
Do not use marks to solve page layout or field direction#
Directional characters act on text processing. They do not replace document structure, fix a physical CSS offset or establish a form control's own direction policy.
If an Arabic page has the wrong overall direction, repair the document's dir declaration. If an icon occupies the wrong edge, inspect the layout rules. Prefixing labels with RIGHT-TO-LEFT MARK does not tell a layout container how to arrange its children. The neighboring topic is covered in dir="rtl" vs CSS direction: rtl.
The same boundary error appears in form workarounds. Adding RLM to the editable value makes a character part of the value. It does not express that a review-note field expects Arabic input or that a title field accepts either direction.
For a title field, put automatic direction on the control:
<label for="clip-title">عنوان المقطع</label><input id="clip-title" name="clipTitle" type="text" dir="auto">If a field is defined as Arabic-only, an explicit RTL direction can better express that requirement. If it contains a known LTR reference, use explicit LTR. Do not seed the field with an invisible character to simulate its intended empty state.
Automatic direction has an empty-state edge case: an empty automatic input is LTR, even on an RTL page. Decide whether that behavior matches the field's contract. Adding a mark to force the initial appearance leaves hidden data in what the user may believe is an empty value.
Test clearing and submitting the field, not just typing into a prefilled example. A display workaround that survives as a nonempty value can interfere with application logic that distinguishes a missing title from a supplied one.
Do not hide a renderer fix in translations#
A translator can make a broken clip-status sentence look right by adding a mark beside its punctuation. If the actual defect is the rendering boundary around an inserted title, that edit moves responsibility to a text resource that may be reused elsewhere.
The translation may later appear in an HTML notification, a native tooltip and a plain-text review digest. Those destinations do not necessarily need the same directional treatment. A hidden repair selected for one destination can be unnecessary or misleading in another.
Keep the localized message and its inserted fields structured. The HTML renderer should be able to isolate the title as a value without asking the translator to maintain invisible opener and closer characters around a placeholder. The plain-text renderer can apply its own destination-specific formatting after message selection.
That does not mean translations can never contain directional marks. Some punctuation context is part of authored text, and a resource may be intentionally dedicated to a plain-text destination. In that case, document the character, its purpose and the required context, and make it visible in review tooling.
Before removing a mark from a resource, inspect every place that uses that resource. A shared string may have acquired a dependency on the mark. Repair those renderers before deleting it, or split the resource when the destinations have different requirements.
Avoid treating translation QA as a request to keep adding invisible characters until every screenshot passes. A review should be able to explain which value owns the punctuation and which layer establishes its directional scope.
Do not store controls just to simplify copying#
Copying a clip reference and copying a complete Arabic review message are different product actions. Give each a defined output contract.
“Copy reference” should return the reference accepted by the lookup service. “Copy review message” may return composed prose intended for a text application. That second action can justify presentation controls if the destination needs them; it does not justify adding them to the reference field itself.
For an HTML reference element such as the one below, reading its textContent returns its text content. Direction markup does not become extra characters in that string:
<span id="clip-reference" dir="ltr">63081752-QZ</span>const referenceElement = document.getElementById("clip-reference");console.log(referenceElement.textContent === "63081752-QZ"); // trueRun the script after the element exists. If the component deliberately inserted literal controls into the text node, those characters would be part of textContent too. MDN's textContent reference documents what this property reads.
A custom copy action should use the authoritative raw field when its promise is to copy an identifier. Do not make it scrape whichever decorated label happens to be visible in the current component. The Clipboard writeText documentation describes the browser API and its secure-context requirements.
Manual selection is a separate path. Test it in supported browsers and the destination applications your users rely on. Do not assume that testing a custom copy button establishes the behavior of every clipboard path, or that a successful paste establishes correct visual ordering on the original screen.
Do not rely on hidden pairs surviving editing and truncation#
A directional isolate uses an opener and a matching closer. If a helper wraps a title and another helper later truncates the already formatted string, it can remove the closer or retain an opener around only part of the intended value. Counting visible letters will not reveal that change.
Search highlighting and editing introduce similar ownership problems. An editor may replace a selected range without preserving its invisible boundary. An update path may wrap an already wrapped title again. The result depends on the operations performed, not merely on whether the initial string was valid.
For HTML, a value element provides a place to update text without embedding directional punctuation in the editable content. Keep the wrapper and replace its text safely through escaped rendering or textContent. Avoid making callers maintain a string protocol they cannot see.
For a genuine plain-text destination, perform content selection and any product-specific shortening first, then construct the destination's complete directional pair. Apply a policy to controls already present in external input before wrapping; adding a trusted outer pair does not sanitize its contents.
Do not assume that all line breaks behave like harmless wrapping. Paragraph boundaries terminate directional scopes, while a soft visual wrap is a different event. A formatter intended for one inline field should not be applied blindly to arbitrary multi-paragraph content. W3C's Unicode controls guidance describes the inline scope and pairing requirements.
Test the final string after all shortening, interpolation and export steps. A unit test for the initial wrapper helper cannot establish that a later operation preserved the boundary.
Do not use overrides as a stronger isolation fix#
LEFT-TO-RIGHT OVERRIDE and RIGHT-TO-LEFT OVERRIDE force directional treatment of characters. They do not simply keep an inserted value separate from its neighbors. A Latin clip label or Arabic title can therefore display in an unintended sequence inside an override.
An engineer trying an override after a mark failed has changed the operation, not strengthened the same repair. The question should be whether the value needs isolation, a known base direction or corrected source data.
Use an override only when the representation explicitly requires forced order and the allowed content is defined. That is a specialized display contract, not a default for filenames, titles or translated messages. The Unicode Bidirectional Algorithm distinguishes overrides from embeddings and isolates.
When reviewing imported strings, make unexpected overrides visible and inspect their source. Their presence alone does not prove malicious intent. It does mean that the visible filename or title may be an unreliable description of its stored sequence.
For source resources and code reviews, use tooling that displays controls or flags them in context. Unicode Source Code Handling explains why a reviewer's visual reading can diverge from the logical structure of program text. A policy should identify the suspicious control and its location, not reject ordinary Arabic writing as suspicious content.
Do not strip legitimate formatter output to satisfy a test#
Avoiding ad hoc controls is different from deleting characters intentionally produced by a locale formatter. Intl.NumberFormat output can contain directional controls and spaces that are not obvious on screen. Their exact presence can vary with configuration and runtime.
If a media dashboard displays a signed audio measurement, keep the numeric measurement separate from its formatted label. Test the source number and formatting options rather than forcing the localized label to match a manually typed ASCII string.
When a formatter result differs from the expected text, inspect code points before changing the test or product. MDN's NumberFormat format reference warns about bidirectional controls and hard-coded comparisons.
If a structured display uses formatToParts(), preserve the literal parts unless the destination contract specifically calls for a transformation. Filtering every literal can remove directional information along with spaces. For a machine-readable export, supply the original measurement instead of trying to recover it by deleting invisible characters from display text.
The same distinction applies to imported observer comments, review notes and filenames. Some fields can validly contain authored controls; a machine reference may forbid them. Use the field's contract to decide what is valid. “No invisible characters anywhere” is not a replacement for understanding those contracts.
Use controls when the destination cannot carry the boundary#
The narrow exception is a text-only destination that requires an inline directional change. A native tooltip attribute, a plain-text notification or a text export cannot contain working child elements around the inserted field.
For a dedicated plain-text clip-reference notification, an explicit LTR isolate can be appropriate:
const clipRef = "63081752-QZ";const LRI = "\u2066";const PDI = "\u2069";const notification = `المقطع ${LRI}${clipRef}${PDI} متاح للمراجعة`;This constructs one destination-specific string. It does not update the stored reference. LRI opens the isolate and PDI closes it; PDF is not the closer for an isolate.
For a title whose direction is unknown, FIRST STRONG ISOLATE can be the appropriate automatic choice. For known RTL content, use the RTL isolate. Keep the choice explicit and test the actual destination's rendering. Do not assume that a text-message viewer, a tooltip and a web paragraph all provide identical surrounding context.
Document where the helper is called, which field it accepts, whether paragraph separators are allowed and how preexisting controls are handled. Keep escapes visible in source so reviewers can see the construction. Hidden literal characters make the policy harder to audit.
The exception ends when markup becomes available again. An HTML preview of the same notification should use its own structured rendering rather than forcing every destination through the plain-text string. The data can be shared; its presentation boundaries can differ.
A string-only component API is not always a plain-text destination#
A custom review banner might accept one text property even though it ultimately renders an HTML paragraph. That is an application API constraint, not a browser limitation. Before adding controls to satisfy it, consider whether the component should accept structured message content.
The media library's banner can model a localized label and an inserted clip reference separately. Its HTML renderer can then put direction on the reference element. Callers should not have to construct an invisible formatting protocol simply because the first version of the component accepted one combined string.
This is especially useful when a component already owns a link, icon and text. It has structure internally; allowing a meaningful inserted value to retain its own boundary is consistent with that responsibility. Keep content escaped and restrict the supported structure rather than accepting arbitrary HTML strings.
Do not broaden the API automatically if the component is genuinely meant to produce plain text for several systems. A notification builder used by a text export may reasonably return a string. In that case, keep an HTML message renderer alongside it instead of assuming the plain-text output is the canonical form for every destination.
Third-party widgets need a separate assessment. Check whether their API supports content nodes, a direction attribute or a rendering callback before adding controls. If they accept only a string and you cannot change that, a documented text-boundary helper can be justified. Test it in the widget itself, including any shortening or escaping the widget performs.
The same reasoning applies to accessible names. A visible button label may be structured HTML even when its aria-label would be a single attribute string. Do not duplicate an already adequate visible label into an attribute merely to create another place that needs directional repair. If an independent accessible label is required, treat it as its own text destination and test its meaning with the intended assistive technology.
A practical review question is whether the string restriction comes from the platform, a third-party contract or your own component design. Platform text surfaces justify text formatting. An editable component interface may justify a structural repair. That choice prevents a workaround introduced for convenience from becoming a requirement imposed on every caller.
Whichever option you select, make its ownership explicit. An HTML component should not unexpectedly insert characters into a supplied identifier, and a plain-text formatter should not return a value that callers mistake for the raw identifier. Name and document the returned representation so it cannot quietly replace the source field.
Replace an existing workaround without breaking another view#
Removing controls safely requires tracing their ownership before changing production data.
Start with one affected field and inspect its value at successive stages: API response, stored client record, localized message, rendered text and copy or export payload. Determine whether the character originates in authoritative content, a formatter or a local helper.
Then identify the consumers. A detail page, a compact search result and an export can share a field while applying different formatting. Removing the old helper only from the detail page can leave another view decorating the value, while deleting a stored mark first can expose a renderer that still relies on it.
Repair the HTML boundary and the raw-value copy path before retiring the workaround for those paths. Give legitimate text-only consumers their own formatter. If stored records require a migration, make the affected field and transformation explicit, retain evidence and check for collisions where exact identifiers are involved.
Do not substitute a global removal expression for this work. Even a correctly targeted Bidi_Control expression cannot decide whether the control is valid in a given field, which component inserted it or whether another destination still needs it.
Normalization is not a substitute either. NFC and NFKC perform Unicode normalization; they are not an instruction to erase bidi controls. MDN's normalize reference explains the operation. A cleanup policy must state the characters and field rules it intends to change.
Verify the data contract and the visible repair#
Make controls visible in diagnostic output rather than trusting a screenshot or ordinary console string:
function revealBidiControls(text) { return text.replace(/\p{Bidi_Control}/gu, character => { const hex = character.codePointAt(0) .toString(16).toUpperCase().padStart(4, "0"); return `[U+${hex}]`; });} console.log(revealBidiControls("\u206663081752-QZ\u2069"));This prints [U+2066]63081752-QZ[U+2069]. It exposes characters, not the correctness of nesting or display. Keep it as a diagnostic helper rather than sending its labelled output to users.
Use separate assertions for the properties the media library promises:
| Workflow | What to verify |
|---|---|
| Reference lookup | The key remains the authoritative raw value |
| Arabic detail page | The value has the intended inline boundary and reading order |
| English detail page | The same component accepts the value without hidden decoration |
| Copy reference | The copied string matches the lookup contract exactly |
| Title editing | Empty values stay empty and updates do not duplicate controls |
| HTML review message | Inserted titles are isolated in markup |
| Plain-text notification | Its final string has intentional, correctly paired controls |
| Localized measurement | Formatter output is preserved according to its display contract |
Compare the failing implementation with the repaired one in the complete Arabic sentence. Repeat with a narrow viewport and after dynamic updates. A value shown alone can hide its interaction with the label or following punctuation.
Test copying into the real destination workflow, including lookup after paste. Separately inspect the visible reference. Logical copying order and visual ordering are different properties, so neither test replaces the other.
A free Ritla scan can report , unisolated bidi hazards: phones, emails, Latin tokens inside Arabic text. Use that finding to locate an HTML boundary worth reviewing, then read dynamic text direction for changing values or Arabic and English mixed text in HTML for sentences with several inserted language runs.