Skip to content

RTL modals and dialogs

Build RTL dialogs with logical close and action placement, reliable modal focus, portal direction, responsive sizing and predictable dismissal.

By , Founder of Ritla

Guide13 min read

A dialog can inherit Arabic text alignment while keeping its close button and actions in their English positions. It can also look fully mirrored while keyboard focus escapes behind the overlay. The first problem concerns layout direction; the second concerns whether the component actually behaves as a modal.

Settle those responsibilities separately. Establish the dialog's direction at its rendered boundary, arrange its content and actions in meaningful source order, then verify opening, dismissal and focus restoration. Moving an overlay to the right does not establish any of those interaction states.

The example here is an urban-tree inventory's export confirmation. It summarizes the selected neighborhood and file format, then lets the visitor cancel or choose to prepare a file. Its close control sits at inline end, which is the left side in Arabic. That is this component's design convention, not a universal requirement that every Arabic dialog must use the same corner or button order.

Establish modality before mirroring the layout#

A modal prevents interaction with the underlying interface while it is active. A dim backdrop is only its visual treatment. Adding role="dialog" and aria-modal="true" to a generic container does not implement background blocking, initial focus or keyboard containment.

Use the native dialog element with showModal() when it fits the application's requirements. The browser puts it in the top layer and makes the rest of its document inert, as described in MDN's showModal() reference. Opening the same element with show() or merely adding open represents a different, nonmodal state.

The WAI modal dialog pattern describes the expected interaction: focus enters the dialog, stays within its interaction sequence and returns to a suitable place when it closes. RTL does not reverse the meaning of Tab and Shift+Tab or turn ordinary buttons into an arrow-key widget.

If an established dialog library already supplies those behaviors, configure and test that component instead of placing a second focus trap around it. Competing focus managers can disagree about the active layer or the element to restore. Your RTL work still includes checking its rendered direction and styles.

Check direction where the dialog is rendered#

A dialog inherits direction through the DOM unless it declares its own boundary. This matters when the Arabic route sets dir="rtl" on an inner application wrapper while an overlay service renders dialogs under the document body.

In that arrangement, the dialog may no longer descend from the RTL wrapper. Arabic text can look superficially readable while the header, footer and text alignment inherit LTR. Inspect the rendered dialog's ancestors and computed direction rather than only the button that opened it.

A framework portal and the browser's top layer are different mechanisms. React's createPortal documentation explains that a portal changes physical DOM placement while retaining React context. A language provider can therefore remain available to the component even though an HTML direction attribute is no longer inherited from its previous DOM location.

By contrast, promoting a native modal to the top layer does not reparent its DOM node. Do not add direction workarounds on the assumption that showModal() moves it under the body.

Prefer document-level lang="ar" and dir="rtl" for an Arabic page. If the application intentionally contains local language boundaries, pass the intended language and direction to the rendered overlay root. Keep those values synchronized during locale changes, including when a dialog is already open.

Direction is not the only styling relationship a portal can change. A selector scoped under an Arabic application wrapper may stop matching the dialog, and inherited typography can come from a different ancestor. Inspect the actual font, line height and component rules at the portal destination. Adding dir="rtl" establishes direction; it does not transfer every style from the trigger's subtree.

For an application embedded in an iframe, remember that native modality applies to the containing document. The surrounding host page is a separate interaction scope. If the product expects the whole host interface to be blocked, coordinate that behavior with the host rather than increasing the dialog's z-index or assuming the iframe's modal owns the parent page.

Build a named dialog with explicit outcomes#

The markup below is a complete small confirmation interface. Its short description can be associated with the dialog without flattening a long document into one announcement. IDs connect the title and description to the dialog instance.

html
<button id="tree-export-open" type="button" lang="ar" dir="rtl">  عرض خيارات التصدير</button> <dialog id="tree-export" class="tree-export" lang="ar" dir="rtl"        aria-labelledby="tree-export-title"        aria-describedby="tree-export-description">  <form method="dialog" class="tree-export__surface">    <header class="tree-export__header">      <h2 id="tree-export-title">تجهيز ملف الأشجار</h2>      <button id="tree-export-close" type="button"              class="tree-export__close"              aria-label="إغلاق خيارات التصدير">        <span aria-hidden="true">×</span>      </button>    </header>     <div class="tree-export__body">      <p id="tree-export-description">        سيشمل الملف الأشجار المطابقة للمرشحات الحالية.      </p>      <dl>        <dt>الحي</dt>        <dd>حي الروابي</dd>        <dt>صيغة الملف</dt>        <dd><bdi lang="en" dir="ltr">GeoJSON</bdi></dd>      </dl>    </div>     <footer class="tree-export__actions">      <button type="submit" value="cancel" formnovalidate autofocus>        إلغاء      </button>      <button type="submit" value="prepare">تجهيز الملف</button>    </footer>  </form></dialog> <p id="tree-export-choice" role="status" lang="ar" dir="rtl"></p>

The close glyph is decorative; its button has an Arabic accessible name. The format remains a known LTR value inside the Arabic dialog. Keep that boundary around the value rather than forcing the whole modal to LTR. Nested LTR content covers more involved file names and technical values.

The dialog's native semantics and modal opening mechanism supply the role and modal state. Avoid duplicating them with unrelated custom wrappers. Use unique IDs when several export controls can exist on one page, and ensure the title references the visible title of the particular instance.

Keep the close control attached to the intended edge#

If an English design pins the close button to the right, its reading-relative equivalent is inline end. Retaining the physical offset keeps it on the right in Arabic too:

Problem

css
.tree-export__close {  position: absolute;  top: 0.875rem;  right: 0.875rem;}

Better, if retaining an absolutely positioned close control

css
.tree-export__close {  position: absolute;  inset-block-start: 0.875rem;  inset-inline-end: 0.875rem;}

This preserves the English position and moves the control to the left in horizontal RTL. The positioned header must also reserve enough space at inline end for the button, or a long title can run underneath it. See absolute positioning in RTL for the containing-block details.

The worked dialog instead keeps title and close button in normal header flow. Use the following as an alternative to the absolute-positioning rules above. It lets the title's height grow without coordinating a fixed top offset and a separate text exclusion area:

css
.tree-export {  box-sizing: border-box;  inline-size: min(38rem, calc(100% - 2rem));  max-block-size: calc(100dvh - 2rem);  margin: auto;  padding: 0;  border: 1px solid currentColor;  overflow: auto;  text-align: start;} .tree-export__header {  display: flex;  align-items: flex-start;  gap: 1rem;  padding: 1rem;} .tree-export__header h2 {  min-inline-size: 0;  margin: 0;} .tree-export__close {  flex: none;  margin-inline-start: auto;  min-inline-size: 2.75rem;  min-block-size: 2.75rem;} .tree-export__body {  padding-inline: 1rem;} .tree-export__actions {  display: flex;  flex-wrap: wrap;  justify-content: flex-end;  gap: 0.75rem;  padding: 1rem;} .tree-export button {  font: inherit;  max-inline-size: 100%;} .tree-export button:focus-visible {  outline: 3px solid currentColor;  outline-offset: 3px;} .tree-export::backdrop {  background: rgb(0 0 0 / 0.45);}

The close control moves as part of the header layout; its multiplication-sign glyph does not need reflection. A close symbol is not a directional next or previous arrow. Review its position and accessible name without treating every icon as artwork that must flip.

Keep display: flex on the header or surface rather than unconditionally applying a display override to the dialog itself. Author CSS can interfere with the native closed presentation. Test the closed state after all design-system styles are applied, not only the open screenshot.

Choose action order and keep the source meaningful#

This example places cancel before prepare in source order. Its normal RTL flex row presents cancel before prepare from the right, and the action group sits toward inline end. The English counterpart can use that same source sequence from the left.

That is an explicit component policy, not a universal rule about where confirmation belongs. An existing product may prioritize another order or follow a platform convention. Choose the policy, represent it in the markup and review it with Arabic readers rather than reversing an arbitrary footer for every dialog.

Avoid a footer whose CSS visually swaps buttons while the source retains an unrelated sequence. Adding positive tabindex values to recreate the screenshot creates another order to maintain. The focus sequence needs to preserve meaning and operation; it does not have to trace every visual edge exactly.

Test a wrapping footer with the actual Arabic labels. A longer prepare action may move to another line even when its English label fits. Keep both controls visible and distinguishable, and make their accessible names agree with the visible action. Do not reduce the Arabic label to a vague word merely to preserve a fixed footer height.

Open and close through the dialog API#

Run this script after the markup exists. It records the chosen outcome in the status element; it does not implement file generation or claim that a download has completed.

js
const treeDialog = document.getElementById("tree-export");const treeOpen = document.getElementById("tree-export-open");const treeClose = document.getElementById("tree-export-close");const treeChoice = document.getElementById("tree-export-choice"); treeOpen.addEventListener("click", () => {  if (treeDialog.open) return;  treeDialog.returnValue = "cancel";  treeDialog.showModal();}); treeClose.addEventListener("click", () => {  treeDialog.close("cancel");}); treeDialog.addEventListener("close", () => {  treeChoice.textContent = treeDialog.returnValue === "prepare"    ? "تم اختيار تجهيز ملف الأشجار."    : "أُغلقت نافذة التصدير دون تجهيز الملف.";});

Submitting a form with method="dialog" closes the containing dialog and uses the submitter's value as the outcome. It does not send the form to an export endpoint. MDN's dialog reference explains that behavior and the distinction between modal and nonmodal opening.

Resetting returnValue before every open prevents an earlier prepare choice from remaining as the result of a later dismissal. The close icon supplies an explicit cancel value too. MDN documents the retained outcome in its returnValue reference.

The footer's cancel button uses formnovalidate. If export options later introduce required inputs, cancellation should still work without satisfying those inputs. The separate close icon also bypasses form submission. Keep validation attached to the action that needs valid data, not to every route out of the dialog.

For an export that must show progress or recoverable errors inside the modal, replace the automatic closing form behavior with the application's submit workflow. Do not close optimistically and then leave a failed export with nowhere to explain the error. Keep repeated submissions, request cancellation and the reported outcome consistent with that workflow.

Keep the reviewed export context attached to the decision#

The neighborhood and format summarize what the prepare action will use. Capture those values from the same export state that produces the request. A confirmation showing the neighborhood selected when it opened must not silently submit a different filter read from global application state when it closes.

Background interaction is blocked during a modal, but background data can still change. A live inventory update, another browser tab or an application event may revise the available export context. Decide whether the dialog operates on its opening snapshot or refreshes that context and asks the visitor to review the changed summary.

On reopening, rebuild the summary from the intended new state and clear any previous pending or error treatment. Resetting returnValue does not reset the application's selected neighborhood, request status or cached export options. Conversely, rebuilding those values should not replace the focused dialog subtree during an ordinary text update.

Include context in the regression case: open the export for the displayed neighborhood, close it, change the selection and reopen. Confirm that the new summary and eventual request describe the same export. Then repeat while a delayed response from the earlier operation arrives. Its result must not overwrite the outcome of the newly opened decision.

Place initial focus according to the content#

The example gives the cancel button autofocus, making its initial focus policy explicit. A small confirmation can reasonably start on an action; a long explanatory dialog may need to start at a heading or paragraph so the beginning is not scrolled out of view.

For structured content with several paragraphs or a table, consider a focusable static heading using tabindex="-1". Preserve the structure for reading instead of placing the whole document in aria-describedby. The short description in this example works because it describes the immediate decision without replacing the body.

An editing modal may instead start on the first field requiring attention. A destructive final confirmation may prioritize the least destructive action. Direction alone decides none of those choices. Keep the opening focus target intentional when content changes from a confirmation into a form.

On dismissal, check that focus returns to the opener. If the opener was removed, disabled or replaced during the operation, choose another meaningful destination in the workflow. A completed action may also lead somewhere more useful than the original trigger. Avoid forcing focus to a stale node simply because the dialog component saved its reference.

Test Tab and Shift+Tab around the modal's boundaries and try to reach background controls. With a custom dialog, verify the actual containment and inert-background behavior rather than assuming aria-modal supplies them. With a native dialog, avoid adding a second trap that competes with the browser.

Define Escape, close and backdrop behavior separately#

The native example retains ordinary close-request behavior, including Escape. MDN's cancel event reference describes the cancelable request: preventing its default action can keep the dialog open. The subsequent close event reports that closure occurred.

Do not treat every closure as confirmation. The explicit prepare result is what authorizes this example's prepare outcome; Escape and the close icon represent cancellation. If unsaved edits require an additional decision, route all relevant dismissal paths through that policy instead of protecting only the footer button.

A dimmed backdrop does not define whether clicking outside closes the dialog. Choose that behavior and test it. Where supported by your target browsers, the native closedby attribute can express different dismissal policies; retain a visible close or cancel action and verify the supported behavior rather than assuming the attribute works everywhere.

For a custom outside-click handler, do not equate every click targeting the dialog element with a backdrop click. Dialog padding can belong to that same element. Test clicks inside the border, text selection, pointer drags beginning inside and ending outside, and touch interaction. An accidental dismissal while reading or selecting text is an interaction defect even when the overlay is positioned correctly.

Keep the whole dialog reachable on small screens#

The sizing example limits width and block size while allowing the dialog to scroll. Dynamic viewport units such as dvh relate to the changing viewport; MDN describes them in its CSS length reference. They do not replace testing with the device's on-screen keyboard and browser controls visible.

Use one deliberate scroll strategy. The simple confirmation scrolls as a whole. A larger editor may instead keep a header and footer outside a scrolling body, but that requires constraints that leave the body usable. A footer covering the last input or error message is not a successful way to keep actions visible.

Avoid a fixed dialog height derived from an English screenshot. Arabic labels, font loading, enlarged text and validation messages can all change the needed space. Test the longest approved title and action wording with the actual font; inspect the close button and focus outline when the title wraps.

Check background scrolling separately from modal interaction. Define whether the page behind the modal should retain its scroll position, and verify restoration when it closes. Do not change the document's direction as a workaround for scrollbar movement or layout shifts. That affects far more than the overlay's width.

Animate without changing the dismissal contract#

The dialog is centered, so it does not need a horizontal entry direction. A small vertical surface transition can use the same motion in either locale:

css
.tree-export[open] .tree-export__surface {  animation: tree-export-appear 140ms ease-out;} @keyframes tree-export-appear {  from { opacity: 0; transform: translateY(0.5rem); }  to { opacity: 1; transform: translateY(0); }} @media (prefers-reduced-motion: reduce) {  .tree-export[open] .tree-export__surface {    animation: none;  }}

The reduced-motion preference removes decoration without removing the dialog's interaction. Closing remains immediate in this example. No animation-completion callback controls when background interaction resumes or focus returns.

If the design uses a horizontal slide, define its intended starting and final positions in each locale. Physical translations do not become logical offsets automatically. A centered dialog sliding from a chosen side is also different from a drawer attached to that side; do not infer one component's motion from the other's convention.

Inspect nested overlays inside the modal#

A format chooser, date picker or tooltip can introduce another portal. If a custom popup is rendered outside the active modal's DOM subtree, it can end up in the inert background rather than among the dialog's usable controls. Raising its z-index does not automatically make it interactive or place it above the browser's top layer.

Use the overlay library's supported modal integration or container option, and inspect where the popup actually renders. Verify its own direction as well as its focus relationship to the dialog. A perfectly RTL modal can still open an LTR chooser under the document body.

Do not make every small popup a second modal merely to bring it forward. A genuine nested modal needs a defined return path to its parent dialog, while a chooser inside the export form should keep the parent workflow understandable. Test Escape from the inner layer, dismissal of the outer layer and focus restoration after each operation.

Verify the opened state and every way out#

Use an Arabic route and the matching English fixture with the same export context. Open the dialog through its real trigger; a closed-page screenshot cannot establish the overlay's layout or interaction.

  1. Inspect rendered language and direction, including portals. Confirm the title, body alignment, close position and action sequence follow the chosen convention.
  2. Verify the accessible name and short description. Check that the close glyph has a translated name and technical values retain their own intended direction.
  3. Open with the keyboard, inspect initial focus, then cycle forward and backward. Confirm that background controls cannot be activated and dismissal restores focus appropriately.
  4. Test prepare, cancel, close icon, Escape and any outside-click policy. Prepare once, reopen and dismiss without preparing to check that the earlier result is not reused. Repeat cancellation with a required export option left empty after extending the form.
  5. Narrow the viewport, enlarge text, delay font loading and display longer content. With an on-screen keyboard present, reach the first and last controls and read all errors.
  6. Exercise reduced motion, rapid reopening, failed operations and nested choosers. Inspect the final open state, focus destination and outcome after interrupted transitions.

Repeat with the browsers and assistive technology the product supports. Separate a misplaced button, an incorrect accessible name and a focus-restoration failure in the report; they need different fixes even when they occur in the same dialog.

Capture failures after opening through the normal application path, including any portal or asynchronous content update. Compare the dialog's outer box with the viewport, then inspect the header, close control and footer separately. A modal can fit within the viewport while a child overflows or overlaps its neighbor. Recording those distinct bounds helps identify whether to change the container's sizing or the internal layout.

A free Ritla scan can report direction-sensitive physical properties without an RTL override with R030, and visible text-bearing elements overlapping with R041. Open the dialog and verify its interaction separately; for a close control still pinned to the wrong edge, continue with absolute positioning in RTL, or use RTL responsive design for a modal that stops fitting on a phone.

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