Skip to content

OTP and verification code inputs in RTL

Build RTL verification-code forms with predictable digit order, paste and autofill support, Arabic-Indic digit handling and clear correction paths.

By , Founder of Ritla

Guide13 min read

A cinema membership portal splits a verification code across six boxes. On the Arabic route, the flex row starts at the right, while its script still treats the first DOM input as the first code position. Pasting the complete code fills only one box. Reversing the field array can make a screenshot look different without making the submitted code correct.

Treat the code as one ordered string. Its characters have fixed positions, and a leading zero belongs to the value. The Arabic page's reading direction does not authorize reversing that sequence or converting it into a number.

Start with one real text input unless the product has a reason to implement the extra interactions of segmented fields. This guide uses the cinema portal's illustrative six-digit code 064927 to explain direction, focus, Arabic-Indic entry, paste, autofill and recovery. The fixture is not a live credential, and six digits is this example's contract rather than a universal OTP length.

Separate code order from page reading order#

An RTL form can have Arabic instructions and actions surrounding an LTR code editor. The code's first character should occupy the first position in its fixed sequence, not whichever slot happens to be closest to the Arabic heading.

There are two different ordering problems to inspect. A container can arrange separate input elements from right to left. The text inside an individual input can also have its own base direction. Correcting one does not establish the other.

The browser behavior supplied for this guide was measured in Chromium 151 on September 24, 2026. Firefox and Safari were not included. In that setup, the first item of a flex row in an RTL context appeared on the right. Ordinary text inputs also inherited direction without a separate declaration.

Do not claim that every unbroken numeric string reverses inside an RTL input. A continuous digit run and a row of separate boxes are different structures. Inspect the exact component and compare its visible positions, DOM order and submitted string.

For this code contract, give the editable code an explicit LTR context while preserving the Arabic context around it. HTML's directionality rules describe the markup behavior. No string reversal belongs in the fix.

Prefer one input to six independent editing surfaces#

This segmented structure creates separate focus stops and inherits an RTL row layout:

Segmented example to diagnose

html
<div class="cinema-code-slots" dir="rtl">  <input aria-label="الخانة الأولى" maxlength="1" value="0">  <input aria-label="الخانة الثانية" maxlength="1" value="6">  <input aria-label="الخانة الثالثة" maxlength="1" value="4">  <input aria-label="الخانة الرابعة" maxlength="1" value="9">  <input aria-label="الخانة الخامسة" maxlength="1" value="2">  <input aria-label="الخانة السادسة" maxlength="1" value="7"></div>
css
.cinema-code-slots {  display: flex;}

The first DOM input starts at the right under the measured flex behavior. Those labels do not implement full-code paste, selection across boxes or a shared accessible editing experience. The example intentionally omits that missing application logic.

One labelled code input

html
<div class="cinema-verification" lang="ar" dir="rtl">  <label for="cinema-code">رمز التحقق</label>  <input id="cinema-code" name="code" type="text" dir="ltr"         inputmode="numeric" autocomplete="one-time-code" required         aria-describedby="cinema-code-help cinema-code-error">  <p id="cinema-code-help">أدخل الرمز المكوّن من ستة أرقام المرسل إلى هاتفك.</p>  <p id="cinema-code-error"></p>  <button type="submit">تحقق من الرمز</button></div>

Place this fragment inside the portal's verification form. It supplies one native value, selection range and focus target. The application still needs format validation and server verification; the markup is not a complete authentication implementation.

If the design wants a segmented appearance, consider whether decoration around one editor can provide it without adding more editing surfaces. Any decorative slots must remain consistent with the real value and must not become a second set of competing accessible inputs.

Use text semantics for a fixed-format code#

The fixture must remain 064927. Parsing it as an integer produces a different representation by dropping its initial zero. A verification code is not a quantity to increment, round or format according to a regional number style.

Use a text input with a keyboard hint rather than selecting type="number" just because this service issues decimal digits. Do not insert thousands separators or translate the code with a general number formatter.

inputmode="numeric" suggests a numeric entry mechanism. It does not validate length, guarantee a digit script or tell the server which challenge the value answers. Test the actual mobile keyboard and preserve paste as an alternative.

The example requires six decimal digits. If a service instead issues letters or another code alphabet, change the keyboard and validation policy accordingly. Do not keep a numeric-only keyboard that makes valid characters inaccessible.

Keep the code LTR independently of the button and help text. A component-wide RTL override on every descendant input can contradict that policy. Inspect the rendered input after initialization, error display and a language switch, not only the template that originally declared it.

Define what pasted formatting the form accepts#

Users can copy the entire code from a message, including presentation spaces. Decide whether the portal accepts 064927, a grouped version such as 064 927, or other separators. Do not make the parser silently guess among several numbers in a copied message.

For this example, accept the six digits directly or separated by ordinary, nonbreaking or narrow nonbreaking spaces. Accept surrounding whitespace, but reject letters, punctuation and complete message prose. That is a deliberate input policy, not a general-purpose SMS extraction algorithm.

Allow replacing the entire current value with a pasted code. Test paste into an empty field, a partially entered value and an active selection. A normal input can support those editing operations without the application distributing characters between nodes.

Do not disable paste to encourage manual transcription. W3C's accessible-authentication guidance explains why users need mechanisms such as paste and automatic filling rather than being forced to remember or transcribe a code.

If the product does extract a code from message text, define that behavior separately and test ambiguous messages. Selecting the first six digits in an arbitrary string can choose a date, reference or part of a destination instead of the intended code.

Normalize supported digit scripts without losing positions#

An Arabic keyboard can enter Arabic-Indic digits. Accepting them requires a conversion policy before ASCII-only format checks run. For the cinema portal, the accepted scripts are Western, Arabic-Indic and Eastern Arabic-Indic digits.

This helper implements the example's six-digit and spacing policy:

js
function readCinemaCode(raw) {  const digits = raw.trim().replace(/[\u0660-\u0669\u06F0-\u06F9]/g, character => {    const code = character.charCodeAt(0);    return String(code - (code >= 0x06F0 ? 0x06F0 : 0x0660));  });  const compact = digits.replace(/[ \u00A0\u202F]/g, "");  return /^[0-9]{6}$/.test(compact) ? compact : null;}

Pass a string such as the input's current value. Both ٠٦٤٩٢٧ and 064 927 produce 064927. An incomplete value or a value containing an unsupported character returns null; it is not padded, truncated or reversed.

Call this from the form's application validation path before sending the code, and validate the accepted representation again on the server. Do not convert the result with Number() after preserving its zero through this function.

The helper maps only the supported digit ranges and removes only the documented internal spaces. It does not indiscriminately delete every nondigit and thereby turn unrelated text into a plausible code. It also does not verify that the code was issued, has not expired or answers the current challenge.

Check the code as delivered as well as entered#

The message and the editor must communicate the same fixed sequence. In the supplied measurements, Western-digit groups separated by spaces in an RTL paragraph displayed in reverse group order. That makes a grouped code beside Arabic prose a separate presentation test from the input itself.

For a plain-text message, keeping the example as one uninterrupted code, 064927, avoids introducing that grouping boundary. In an HTML message or an on-page example, isolate a grouped value separately from its Arabic label. Do not rearrange the code's characters on the server to compensate for a particular message preview.

Test the actual SMS or email presentation the product uses, including notification previews and copying the code into the form. The string copied from a message can retain its logical order even when its visible grouping is misleading. Compare what users see with what the input receives rather than accepting a copy test as proof of both.

If an SMS retrieval integration requires a machine-readable message format, preserve that format while translating the explanatory prose. A localizer should not change code digits, origin information or protocol punctuation merely to make the whole message resemble an ordinary Arabic sentence. Verify retrieval with the final localized template.

Accepted digit scripts need display testing too. In the supplied measurements, spaces between Arabic-Indic digit groups resolved RTL even inside an LTR boundary. The normalization helper establishes a canonical string, but it does not by itself establish how the unnormalized editing value looks before submission. Inspect that interval and choose whether to display a compact normalized value at a deliberate edit boundary. If the application rewrites it, preserve a usable selection and do not disturb an active composition.

Keep raw-entry constraints compatible with normalization#

The input example deliberately omits a raw six-character maximum. A grouped paste can contain more than six characters before spaces are removed. A native maxlength="6" can prevent the application from receiving all of that input, even though its normalized code has the required length.

MDN's maxlength reference describes a constraint on the control's text length. It is not a count of normalized code digits. If you set an entry limit, account for every representation the product promises to accept and reject extra normalized digits instead of silently slicing them away.

An ASCII-only raw pattern can similarly reject Arabic-Indic entry before the custom normalization path handles it. Coordinate native constraints with application validation rather than stacking incompatible rules onto the field.

Test a six-digit value with a leading zero, the grouped form, each accepted digit script, a shorter value and a longer one. Include unexpected punctuation and a letter resembling a digit. A complete-looking string should not pass merely because cleanup discarded the evidence that it contained other content.

Do not apply this conversion policy to every credential field. Digit mapping suitable for a decimal OTP can change a password. Keep code normalization scoped to this particular code alphabet and service contract.

If boxes are required, define their interactions before coding#

Segmented inputs need more than an LTR wrapper. Keep logical positions in code order and decide how every editing action changes the shared value.

ActionDecision the component must implement
Enter a characterReplace the selected position or insert according to a documented model
Paste a full codePopulate the whole code rather than truncating into one slot
Paste over existing contentReplace a deliberate range or the whole value predictably
Backspace in an empty slotMove and delete according to one consistent policy
Arrow navigationMove through visual positions without reversing stored characters
AutofillAccept a complete value and synchronize all displayed slots
Change the code lengthRebuild positions without retaining stale characters

Give the group an Arabic question and make each position understandable if it remains a separate focus target. Do not label positions using physical directions such as “the box on the right” when the value itself follows a fixed LTR sequence.

Avoid positive tab indexes and array reversal as a substitute for a coherent model. A screenshot cannot establish that focus, visible position and serialization agree. Use the leading-zero fixture to trace each logical position through all three.

If the implementation cannot support those operations without surprising focus jumps, return to one input. The segmented look does not justify losing basic editing and paste behavior.

Focus should support correction rather than chase each digit#

For one input, let the user place the caret and select a range. Do not force the caret to the end after every input event. Correcting the middle of a code should not require clearing the entire value first.

If application code rewrites a formatted value, map selection positions to the changed string. setSelectionRange() can set a text input's selection, but it does not infer the intended offsets after normalization or separator removal.

For several inputs, test typing ahead, returning to a previous slot and replacing a selected digit. Automatic advance should not steal focus after an invalid character or make a correction land in a different slot. Test the policy with keyboard navigation and touch separately.

Do not assume that every edit produces one key event with one character. Paste, autofill and input methods can supply text through other paths. compositionend marks the end or cancellation of a composition session; do not redistribute or submit an unfinished composition based only on its temporary length.

Check the focus destination after a failed verification. Keep the current code available for correction when that is the chosen flow, and do not repeatedly pull focus away from the resend or destination-change action while the user is trying to recover.

Treat autofill as a complete-value entry path#

Use autocomplete="one-time-code" for the actual code field. MDN defines that token; it is distinct from a saved password or the telephone number used to receive the message.

The token is a hint, not a guarantee that every browser, message channel and device will offer a code. Test the supported combinations and keep manual entry and paste available when no suggestion appears.

For segmented interfaces, verify where the autofill suggestion inserts its full value. Applying the token to several one-character inputs does not itself implement distribution into the component's logical positions. One full-code input avoids that particular synchronization problem.

Send autofilled content through the same normalization and validation path as typing. Do not accept a value merely because a platform suggested it, and do not rely on a custom keyboard handler to make the field ready for submission.

Application-driven population needs explicit synchronization too. Setting a value through JavaScript does not itself fire the input event. Update any decorative slots, completion state and error presentation through the component's shared value path.

Add SMS retrieval as an enhancement, not the only route#

Where the portal uses SMS and the browser supports it, the WebOTP API can help retrieve a code from a specially formatted message. It requires a secure context in supporting browsers and has platform-specific availability.

Treat this separately from the HTML autocomplete token. WebOTP has an application request and message-format contract; adding an input attribute does not implement that API. Google's SMS OTP form guidance describes those integration concerns.

Keep retrieval failure, user cancellation and unavailable support from trapping the user on an uneditable screen. A working manual code field should remain available. Test the same flow when no API result arrives.

If the user changes the destination, resends the code or leaves the screen, ensure a delayed retrieval result cannot overwrite a newer challenge's input. Associate retrieved content with the operation that requested it and cancel obsolete work where the integration permits it.

Do not broaden this into a promise that retrieving a message completes verification. The server still decides whether the submitted code answers the current challenge. Client retrieval and server acceptance are different stages.

Keep Arabic guidance tied to the current challenge#

State what the code is for and where it was sent without making users infer it from six empty boxes. For a masked phone destination, give the displayed suffix its own boundary:

html
<p lang="ar" dir="rtl">  أرسلنا رمز الدخول إلى الرقم المنتهي بـ  <bdi dir="ltr">7286</bdi></p>

Use the destination associated with the active challenge, not a stale account profile or an edited field that has not yet received a new code. Bidi isolation covers the text boundary; application state determines which destination belongs there.

Keep the length instruction consistent with the code the server issued. A reused component should not still ask for six digits when a different verification method expects another format.

When changing language on this screen, retain the active challenge, destination and any permitted in-progress entry. Translate the interface around that state. Do not issue a new code merely because a locale change remounted the component, or keep displaying the old destination after switching to another challenge. Test a language change while a resend request is pending and when the input contains only part of the code.

Distinguish missing, incomplete, malformed, rejected and expired codes in reviewed Arabic copy where the service provides those conditions. For example, أدخل رمز التحقق كاملاً. asks for completion, while انتهت صلاحية الرمز. اطلب رمزاً جديداً. describes a different recovery action.

Do not translate a server exception literally or show English provider text inside the Arabic correction flow. W3C's notification guidance explains associating feedback with controls and making correction possible.

Make submit, retry and resend states agree#

The example uses an explicit verification button. Entering the expected number of characters does not prove the code is correct, and an automatic request on that boundary can interrupt a paste or a mid-string correction.

If the product uses automatic submission, test every complete-value source, including typing, paste, autofill and retrieval. Prevent duplicate requests for the same attempt, and ensure an earlier response cannot reset a later editing state. A return to manual correction should be visible and usable.

Separate a locally incomplete value from a server rejection. Do not show “incorrect code” merely because the user has entered only part of it. A network failure also does not establish that the code itself was wrong.

After resend, follow the server's actual policy for old and new challenges. Do not promise that the earlier code still works or has stopped working unless that is the implemented behavior. Update the instruction, destination and any countdown from the same challenge state.

Use Arabic status text for pending verification and resend results. W3C's status-message guidance covers updates conveyed without taking focus. Avoid announcing a countdown every second when a less intrusive update can communicate when another request becomes available.

Verify the entire code journey on supported devices#

Use the same fixture across display, normalization and serialization tests. Then use controlled server fixtures for rejection, expiry and resend behavior. A format helper returning six digits is not proof of authentication success.

CaseWhat to verify
Code beginning with zeroEvery position survives entry and submission as text
Arabic-Indic and Eastern Arabic-Indic digitsConversion follows the accepted alphabet without reordering
Grouped pasteNo truncation before spacing normalization
Extra digit or unexpected characterRejection rather than silent slicing or guessing
Selection and middle correctionPredictable caret and focus behavior
Autofill and SMS retrievalFull value follows the shared validation path
Resend or changed destinationInput and feedback belong to the active challenge
Rejection and network failureArabic correction path matches the actual condition
Narrow viewport and open keyboardCode, errors and recovery actions remain reachable

For segmented designs, also trace DOM index, screen position, focus and submitted position for each character. For one input, inspect editing and visible order separately from the resulting string. The supplied measurements show why a matching copied value alone cannot establish visible bidi presentation.

Include screen-reader and keyboard review of the actual verification task, not only an empty component. A user must be able to paste or enter the code, discover an error and reach resend without the component repeatedly moving focus elsewhere.

A free Ritla scan can report R071: Untranslated form labels / validation / help text, and R011: Untranslated interactive labels (buttons, nav/footer links, menu items, tabs). Code order, paste distribution, autofill and retry state need the task tests above; continue with RTL flexbox for slot-container order or RTL elements overlapping on mobile when code fields and recovery actions do not fit.

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.

Scan a page freeBrowse the checks