How to fix horizontal overflow in RTL

Find the exact element causing RTL horizontal overflow, understand why it escapes on the left, and fix the layout without hiding content.

Guide14 min read

Horizontal overflow on an Arabic page is easy to hide and surprisingly hard to fix correctly. A child positioned with left, a drawer translated in the LTR direction, a flex item that refuses to shrink or a late-loading validation message can add hundreds of pixels to the document without looking broken at the initial scroll position.

RTL also changes which off-screen content becomes reachable. In Chromium 151, measured on 2026-09-17, content past the left edge of an RTL page increased the document's horizontal scrollable width. Equivalent content past the right edge did not. An LTR page behaved in the opposite direction.

The repair is not overflow-x: hidden. First prove where the overflow belongs, identify the exact box that expanded the scrollable area and fix the assumption that placed or sized it incorrectly.

Confirm that the document is overflowing#

Start by separating three failures that look similar:

  • Document overflow: the whole page scrolls sideways.
  • Component overflow: a table, carousel or code block has its own horizontal scroller by design.
  • Clipping: content is cut off, but no scroll range exposes it.

Measure the root in the failing state:

js
const root = document.documentElement;

console.table({
  clientWidth: root.clientWidth,
  scrollWidth: root.scrollWidth,
  overflow: root.scrollWidth - root.clientWidth,
});

A positive difference shows that the document has horizontal scrollable overflow. Record the viewport, route, zoom, browser and exact state before changing CSS. If the issue appears only after a menu opens or an error loads, measure before and after that transition.

Do not rely on a visible scrollbar. Operating systems can use overlay scrollbars, mobile browsers may hide them and root CSS can suppress them. Drag horizontally, use the browser's scroll controls and compare dimensions.

R040 reports document-level horizontal overflow. A report confirms the symptom; it does not tell you which descendant owns the fix.

Understand the RTL overflow edge#

The root's base direction determines the inline start and end used by page flow. Under RTL, inline start is on the right and inline end is on the left. The browser's scrollable overflow region is not a symmetric canvas extending forever in both directions.

This explains a confusing reproduction pattern. An absolutely positioned element at left: -300px added roughly 300 pixels of scrollable width to the measured RTL page. The same element past the right edge did not. Under LTR, the result reversed.

The lesson is not “negative left is always wrong.” The lesson is to inspect the side that extends the scroll range for the current direction. A debugging routine built only for LTR often checks the right edge and misses the leftward culprit.

Use logical language in the bug report. Record whether the box crosses inline start or inline end, then include its physical bounding rectangle. That stays understandable when comparing LTR and RTL versions.

The CSS overflow documentation distinguishes visible, clipped and scrollable overflow. The page can also contain an intentional scroller, so identify the scroll container before deciding that any oversized content is a defect.

Reproduce the state that creates the overflow#

Overflow often depends on content or timing. A clean homepage at desktop width proves little.

Test at:

  • the minimum supported viewport;
  • widths immediately before and after navigation or card breakpoints;
  • a medium width where labels wrap;
  • wide desktop;
  • 200 percent browser zoom;
  • the supported font-size or text-spacing settings.

Then force dynamic states:

  • multiline validation and server errors;
  • open menus, dialogs, drawers, tooltips and toasts;
  • loading, empty, failed and populated data;
  • a long Arabic navigation label;
  • a phone, email, URL and long order reference;
  • web-font fallback followed by the loaded Arabic font;
  • locale switching without a full reload;
  • sticky headers and columns after scrolling.

Wait for network content and fonts before taking the final measurement. A fallback font may fit while the intended Arabic font is wider, or the reverse. A dialog may fit until a translated server error replaces its short placeholder.

Record the first transition that increases scrollWidth. That narrows ownership. If overflow begins when a drawer closes, inspect its transform. If it begins when results load, inspect the data row and long values.

Find the element that crosses the viewport#

Use DevTools to inspect boxes near both horizontal edges. A quick console query can produce candidates:

js
const viewportWidth = document.documentElement.clientWidth;

const candidates = [...document.querySelectorAll("body *")]
  .filter((element) => {
    const style = getComputedStyle(element);
    if (style.display === "none" || style.visibility === "hidden") {
      return false;
    }

    const rect = element.getBoundingClientRect();
    return rect.left < -0.5 || rect.right > viewportWidth + 0.5;
  })
  .map((element) => {
    const rect = element.getBoundingClientRect();
    return {
      element,
      left: Math.round(rect.left),
      right: Math.round(rect.right),
      width: Math.round(rect.width),
    };
  });

console.table(candidates);

This is a candidate list, not a verdict. A child inside an intentional horizontal table scroller may cross the viewport without expanding the document. An off-canvas drawer may be hidden correctly by its component container. Check each candidate's nearest scrolling and containing ancestors.

The script also does not list pseudo-elements as separate DOM nodes. If the candidate box looks correct, inspect ::before, ::after, outlines, transforms and generated decoration in the Styles panel.

Another useful temporary diagnostic is an outline:

css
* {
  outline: 1px solid rgb(255 0 0 / 0.2);
}

Use it only while debugging. It makes a wide child or unexpected translated box obvious, but it does not identify which CSS declaration caused the geometry.

If the page is large, disable major regions in DevTools one at a time. When the root scrollWidth returns to clientWidth, the removed region contains the culprit. Continue narrowing until one component or pseudo-element remains.

Inspect the box, its parent and its containing block#

The element outside the viewport is not always the element that needs the fix. A badge may be positioned correctly relative to the wrong containing block. A long child may overflow because its flex parent refuses to shrink. A transformed drawer may be correct while its clipping wrapper is missing.

For each candidate, record:

  1. its bounding rectangle;
  2. computed width, min-width and max-width;
  3. margin, padding and border on both inline edges;
  4. position, insets and transform;
  5. white-space, overflow-wrap and text content;
  6. its containing block and nearest overflow ancestor;
  7. flex or grid sizing on the parent;
  8. whether the geometry changes after fonts or data load.

Toggle one declaration at a time. Disabling transform, min-width, a physical inset or a negative margin can expose the owning assumption without committing a patch.

Do not stop when the scrollbar disappears. Confirm that required content remains visible, reachable and aligned, and that the LTR version still works.

Replace physical positioning with logical positioning#

An element attached to logical start or end should use logical insets and spacing. The account menu in a site header is a classic case:

Problem

css
.account-menu {
  position: absolute;
  top: 100%;
  right: 0;
  min-inline-size: 16rem;
}

In English the account trigger sits at the right end of the header, and right: 0 lines the menu up with the trigger's right edge so it opens leftward into the page. In Arabic the header flows the other way and the trigger ends up at the left end. right: 0 still lines the menu up with the trigger's right edge, so the menu now extends leftward, past the left edge of the viewport. That is precisely the edge that adds scrollable width on an RTL page, and the Arabic header gains a sideways scroll only while the menu is open.

Direction-aware

css
.account-menu {
  position: absolute;
  top: 100%;
  inset-inline-end: 0;
  min-inline-size: 16rem;
}

The change is correct because the menu belongs to the trigger's ending edge in both directions. Keep physical left or right when the element represents a fixed physical side, such as a marker on a map.

Inspect negative margins and absolute offsets as a pair. Converting left to inset-inline-start while leaving margin-left can still push the child across the wrong edge. R030 reports direction-sensitive physical properties without an RTL override.

For an asymmetric corner, border or divider, use the corresponding logical property. A corner radius itself rarely creates root overflow, but mismatched physical geometry is often evidence that the component was authored only for LTR.

Fix transforms at the state that fails#

Transforms keep their authored physical direction. translateX(-100%) remains a leftward movement under RTL.

An inline-start drawer needs a direction-aware closed offset:

css
.drawer {
  position: fixed;
  inset-block: 0;
  inset-inline-start: 0;
  --closed-x: -100%;
  transform: translateX(var(--closed-x));
}

.drawer:dir(rtl) {
  --closed-x: 100%;
}

R033 reports a translateX that moves the element off-screen in RTL. Test closed, entering, open and exiting states. A component can look correct while open and expand the page only during its exit animation.

Check the wrapper that owns clipping. If the drawer is intended to move inside a bounded component, that component may need the overflow rule rather than the document. Do not add root clipping to compensate for a missing local boundary.

Transforms used for decorative centering also deserve review. A common left: 50% plus translateX(-50%) pattern expresses physical centering and may remain valid. Do not reverse it merely because the page is RTL. Judge the geometry by meaning, not by the presence of a negative number.

Fix width calculations and the box model#

Several LTR-neutral sizing patterns create overflow in both directions but become more visible on the RTL scrollable edge.

Width plus padding#

With the default content-box model, width: 100% does not include padding or border:

css
.panel {
  width: 100%;
  padding-inline: 1rem;
}

Use border-box sizing or allow the block to use its normal auto width:

css
.panel {
  box-sizing: border-box;
  inline-size: 100%;
  padding-inline: 1rem;
}

Viewport width inside a container#

inline-size: 100vw sizes against the viewport, not the parent content box. Inside a padded layout it can extend beyond the container. It can also disagree with the document's usable width around scrollbars. Use inline-size: 100% when the intended boundary is the parent.

A true full-bleed section is different. Keep the breakout logic isolated, test it under both directions and measure the root at every breakpoint. Do not reuse full-bleed CSS for ordinary cards.

Fixed widths#

A fixed width may fit a short English label and fail with approved Arabic content. Prefer max-inline-size, flexible tracks and content-driven block size. R043 reports a fixed-pixel-width container whose Arabic text exceeds the available space.

Let flex and grid children shrink#

Flex and grid items can keep an intrinsic minimum size that prevents the track or row from shrinking. The usual symptom is a card whose outer width looks responsive while one text or data child pushes the document wider.

For a flexible child that may contain long content:

css
.result-row {
  display: flex;
  gap: 0.75rem;
}

.result-row__content {
  min-inline-size: 0;
  flex: 1 1 auto;
}

min-inline-size: 0 allows the item to become narrower than its automatic minimum. It does not decide how text inside should wrap; set that separately based on the data.

For grid, a flexible track may need an explicit zero minimum:

css
.result-card {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 1rem;
}

Without the zero minimum, an unbreakable value in the flexible track can hold the layout wider than its container. Inspect the computed track sizes and the overflowing child before changing the template.

Adding flex-wrap: wrap can be correct for a toolbar designed to form multiple rows. It is not a universal fix for a child that should shrink within one row. Wrapping can also separate a label from its action or change keyboard relationships.

Handle long text according to its data type#

Different strings need different overflow behavior.

Normal Arabic prose should wrap at natural opportunities. A long URL or unbroken user string may need emergency wrapping:

css
.user-content,
.validation-message {
  overflow-wrap: anywhere;
}

Do not apply aggressive breaking to every value. An order ID, phone number or account number may become harder to verify if it breaks at arbitrary positions. For a value whose exact sequence matters, consider a bounded horizontal scroller, a copy action or a layout that gives the value its own line.

An IBAN is a good fixture for this section because it is long, has no natural break points that a customer would accept, and has to be read back exactly:

html
<p dir="rtl">
  الآيبان <bdi>SA44 2000 0001 2345 6789 1234</bdi>
</p>

Measured in Chromium 151, this IBAN displays in order even without the <bdi>, because its leading Latin letters start a left-to-right run that carries the digits with it. Keep the isolation anyway: the component cannot know that every value it renders will start with letters. R021 reports unisolated phones, emails and Latin tokens inside Arabic text. Isolation does not fix a sizing bug, but it ensures the value being measured displays in the intended order.

For truncation, confirm that the product permits information to be hidden and that the full value remains available. R042 reports clipped text when content is wider than its box with hidden overflow and no ellipsis. An ellipsis can clarify truncation, but it does not make hidden required content acceptable.

Constrain media and embedded content#

Images, video, canvases, iframes and charts often bring an intrinsic or authored width larger than the viewport.

For responsive media that should fit its parent:

css
img,
video,
canvas {
  max-inline-size: 100%;
  block-size: auto;
}

Do not apply block-size: auto blindly to embedded surfaces whose aspect ratio or drawing buffer is controlled elsewhere. Give charts and iframes a component-specific wrapper with a defined responsive contract.

SVG root dimensions and internal graphics do not mirror under RTL. A wide chart may need a local scroller rather than compression if labels and points would become unreadable. That scroller must contain its overflow so it does not enlarge the document.

Test media after the final source loads. A placeholder can be small while the selected image or embedded report has a fixed width.

Make wide tables intentional scrollers#

A dense data table may legitimately be wider than a phone. The correct result is a bounded table scroller, not a page-level scrollbar and not crushed columns.

html
<div class="table-scroll" tabindex="0" aria-label="جدول الطلبات">
  <table>
    <!-- rows and cells -->
  </table>
</div>
css
.table-scroll {
  max-inline-size: 100%;
  overflow-x: auto;
}

.table-scroll table {
  min-inline-size: 48rem;
}

The accessible name and focus treatment need product review. The example shows that the wrapper, not the document, owns horizontal scrolling.

In an RTL scroll container, scrollLeft was 0 at inline start and negative toward the end in Chromium 151. Scripts that clamp the value to zero or expect a positive maximum can break next and previous controls.

Test the table from start, middle and end with keyboard, touch and any provided controls. Confirm sticky columns and shadows remain attached to the intended edge.

Check fixed, sticky and portaled UI#

Fixed and sticky elements can create or hide overflow independently of the main content flow. Review floating action buttons, cookie banners, chat launchers, sticky table columns and fixed headers at both horizontal edges.

A portaled dialog or tooltip may be mounted under body instead of inside the component that triggered it. If root direction is correct, it inherits the document context. It still needs a viewport-aware placement strategy and a maximum inline size.

Use content-driven constraints:

css
.toast {
  inset-inline-end: 1rem;
  max-inline-size: min(28rem, calc(100% - 2rem));
}

Open overlays near every corner and with the longest approved Arabic text. A placement library may flip a tooltip to stay visible, but verify the actual computed position rather than assuming the library understands the page's logical edge.

R041 reports visible text-bearing elements that overlap. Overlap and document overflow often share the same bad containing block or fixed width, so inspect both after the repair.

Do not hide root overflow as the fix#

These rules suppress evidence:

css
html,
body {
  overflow-x: hidden;
}

They can make a release screenshot look clean while an off-screen button, validation message or menu remains unreachable. Root clipping can also conceal the exit path of an incorrectly translated drawer.

Local overflow is appropriate when the component's contract calls for it: a cropped decorative image, a carousel viewport or a wide table wrapper. The boundary must contain only content that is allowed to clip or scroll.

After removing the root rule, reproduce the bug and fix the owning child. Then restore any local overflow behavior that has a documented purpose. Measure the root again and navigate the page with a keyboard to confirm nothing required is trapped outside the visible area.

Use the failure signature to narrow the cause#

The amount and timing of overflow often point toward a small group of declarations. Treat these as hypotheses to verify in DevTools, not automatic diagnoses.

Failure signatureLikely causesFirst inspection
Only RTL overflows, toward the leftPhysical inset, negative margin or wrong translateX()Bounding rectangle, insets and transform
Overflow roughly equals combined inline paddingwidth: 100% under content-box sizingComputed width, padding and box-sizing
Appears at one breakpointFixed width, minimum width or absolute layout ruleActive media query and computed min size
Appears after validationLong error, no wrap opportunity or flex child minimumError box, white-space, overflow-wrap, min-inline-size
Appears after the web font loadsNew text metrics inside a fixed constraintRendered font, text width and container width
Exists only while a drawer closesDirectionally wrong keyframe or missing local clippingAnimation state, transform and wrapper overflow
Page is fixed but a carousel control failsComponent scroll logic, not document overflowScroll container range and visible item
Scrollbar disappears but content is missingRoot clipping hid the symptomDisable root overflow rule and locate the child

Measure the overflow delta as well as the candidate width. A 16-pixel delta often points to spacing or box-model math, while a full viewport-width delta often points to an off-canvas panel or 100vw child. These are clues, not universal constants.

Compare the same state under LTR. If the overflow changes edges, direction-sensitive positioning is likely. If both locales overflow by the same amount, start with intrinsic sizing, full-viewport widths and unbreakable data before adding any RTL override.

Keep the smallest reproduction that retains the defect. Remove decoration, then content, then layout rules until the scroll width changes. The last removed dependency usually identifies whether the cause is geometry, text measurement or state transition.

Add a regression test at the failing transition#

A root-width assertion is useful when it runs in the state that originally failed. It should allow a small rounding tolerance and report both dimensions when it fails.

js
async function documentOverflow(page) {
  return page.evaluate(() => {
    const root = document.documentElement;
    return {
      clientWidth: root.clientWidth,
      scrollWidth: root.scrollWidth,
      delta: root.scrollWidth - root.clientWidth,
    };
  });
}

await page.goto("/ar/checkout");
await page.evaluate(() => document.fonts.ready);

// Trigger the state that previously introduced overflow.
await page.getByRole("button", { name: "إتمام الدفع" }).click();

const result = await documentOverflow(page);
expect(result.delta).toBeLessThanOrEqual(1);

The Arabic button label is approved test data in this example. A team that does not want localized copy in selectors can use a stable accessible contract or test identifier, as long as the test still verifies the Arabic route and visible state.

Run the assertion at the supported narrow viewport and after asynchronous content settles. If the original defect occurred during animation, sample the transition or assert the drawer's closed bounding box as well. A final-state check will miss a temporary box that enlarges the document while exiting.

Do not fail because an intentional inner scroller has content wider than itself. The root should remain bounded while the table or carousel reports its own larger scrollWidth. Add separate interaction tests for that component's start, middle and end.

When the assertion fails, attach the root dimensions, viewport, route and candidate-element output. A bare screenshot of a sideways page is slower to diagnose than the same screenshot paired with the box that crossed the edge.

Verify the repair, not only the scrollbar#

A fix passes when the root no longer overflows unexpectedly and the content still works.

Retest:

  • Arabic and English routes;
  • minimum, intermediate and wide viewports;
  • 200 percent zoom;
  • fallback and loaded fonts;
  • loading, empty, error and populated states;
  • closed, entering, open and exiting overlays;
  • long Arabic text and mixed-direction values;
  • keyboard focus through the affected component;
  • both horizontal extremes of intentional scrollers.

Capture root dimensions before and after the triggering transition. Add a regression assertion for critical routes, but keep state coverage realistic. A homepage-only scrollWidth check will not catch a checkout error dialog that creates overflow after submission.

Ritla reports document-level horizontal overflow with R040 and related overlap, clipping and fixed-width failures with R041, R042 and R043. A free scan measures the page it renders at its own viewport, so treat a clean result as evidence for that state only, and use DevTools to identify the component and declaration that owns each fix.

Most overflow traces back to one of two habits, and both have guides: physical CSS, covered in RTL CSS, and the patterns collected in common RTL bugs.

Checks in this guide

Show every check in this guideShow fewer

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.