Skip to content

RTL elements overlapping on mobile

Trace and fix overlapping Arabic text, buttons and panels at phone widths by checking flow, positioning, intrinsic size and direction-aware CSS.

By , Founder of Ritla

Guide14 min read

The Arabic title and filter button are both visible on desktop. On a phone, the button sits on the title. Raising the title's z-index makes the title cover the button instead. We have achieved a new stacking order and precisely no new space.

RTL elements overlap on mobile when a layout keeps a position or size assumption from a wider view. An absolutely positioned control may still use right from the English design. A negative margin may pull a badge toward the wrong neighbor. A flex item may refuse to shrink when Arabic text wraps. At phone width, there is no spare space to disguise any of this.

The task is to find which box stopped reserving room for which other box. Once you know that, the fix is usually a change to flow, a logical inset, a flexible track or a deliberate stacked layout. This guide stays with collisions between elements. If the main symptom is that the entire page scrolls sideways, use the horizontal overflow guide for the page-level investigation.

Identify the two boxes that collide#

Start with a screenshot at the failing width, then inspect the two elements in DevTools. A heading apparently “under” a button can mean several things:

What the boxes doLikely causeFirst useful check
Both occupy the same coordinatesAbsolute or fixed positioning, negative margin, transformWhich one is outside normal flow?
Text paints over its neighboring boxFlex or grid minimum size, no wrapCan the text item shrink or stack?
One box is in the right place but covers the otherFixed header or bottom barWas space reserved for the overlay?
Only the narrow layout collidesA breakpoint overrideWhich property changes at that width?

Do not begin with z-index. It decides which overlapping thing appears in front. It does not prevent overlap. MDN's stacking-context guide explains the paint order; the element's position and size come from other rules. If the button obscures a word, sending the button behind the word may also make the button impossible to use.

Look at the computed position, inset, margin, transform, display, width and minimum size for both elements. Check their ancestors too. An absolutely positioned button is placed relative to a positioned ancestor, while a fixed button is positioned in a different context. The box that looks guilty in the screenshot may merely be standing where the stylesheet told it to stand.

For a quick geometric lead, compare the title and button rectangles in the browser console:

js
const title = document.querySelector(".market-header__title");
const filter = document.querySelector(".market-header__filter");
const a = title.getBoundingClientRect();
const b = filter.getBoundingClientRect();

const intersects =
  a.left < b.right && a.right > b.left &&
  a.top < b.bottom && a.bottom > b.top;

console.log({ intersects, title: a, filter: b });

Run that on a page containing those two classes. getBoundingClientRect() returns viewport-relative box geometry. An intersection is evidence to inspect, not a verdict: a decorative badge may overlap intentionally, and a text box can contain empty space that intersects a button without painted letters touching it. The final check is whether text or an interactive target is obscured. R041 reports visible text-bearing elements that overlap; it does not say every pair of intersecting rectangles is a defect.

A filter button with no reserved space#

Consider a directory of stalls at a weekly market. Its mobile header shows the directory title and a filter control:

html
<header class="market-header" lang="ar" dir="rtl">
  <div class="market-header__copy">
    <p>دليل السوق الأسبوعي</p>
    <h2 class="market-header__title">الأكشاك المشاركة في سوق الحي</h2>
  </div>
  <button class="market-header__filter" type="button">
    تصفية حسب نوع المنتج
  </button>
</header>

An English desktop design might place the filter in the top-right corner and reserve space for it with right padding. A narrow-width override removes that reserved area:

Problem

css
.market-header {
  position: relative;
  min-block-size: 5rem;
  padding: 1rem 12rem 1rem 1rem;
}

.market-header__filter {
  position: absolute;
  top: 1rem;
  right: 1rem;
}

@media (max-width: 42rem) {
  .market-header { padding: 1rem; }
}

At desktop width, the right padding gives the control a reserved area. At narrow width, that reservation disappears, while the button remains absolutely positioned. Absolutely positioned elements do not reserve space in normal flow. In RTL, the title begins at the right side, exactly where the button remains anchored. The shorter English title might still fit around the control by accident; the Arabic title has no such obligation.

A flow layout is a better fit when both title and action need room:

Better

css
.market-header {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 1rem;
  min-block-size: 5rem;
  padding: 1rem;
}

.market-header__copy { min-inline-size: 0; }

@media (max-width: 42rem) {
  .market-header { grid-template-columns: minmax(0, 1fr); }
  .market-header__filter { justify-self: start; }
}

On a wide LTR page, the copy remains on the left and the filter on the right. On a wide RTL page, the first grid item starts at the right and the filter sits after it toward the left. At narrow width, the filter follows the title on its own row in both directions. The DOM and keyboard order remain title, then control. The layout now reserves space for both.

Test the exact Arabic title with the button visible, then repeat with a longer localized filter label. At widths just above and below 42rem, check that neither element crosses the other's box. If the header can appear inside a narrow side panel on a wide viewport, consider a component-width rule; a viewport media query cannot see that local constraint.

When absolute positioning is intentional#

Not every positioned control should become a grid item. A small close control over a panel, or a badge attached to a thumbnail corner, may genuinely belong in an overlay. In that case, keep the visual relationship explicit and reserve the space the content needs.

For a filter that belongs at the inline end of the header, the LTR physical right offset and its matching reserved padding can be expressed logically:

css
.market-header {
  position: relative;
  padding-inline-end: 12rem;
}

.market-header__filter {
  position: absolute;
  inset-block-start: 1rem;
  inset-inline-end: 1rem;
}

This preserves the original left-to-right relationship: filter and reserved space stay on the right. In RTL they move together to the left. Logical inset properties map inline start and end according to the element's direction. right does not do that by itself. A direction-sensitive physical property without an RTL override is the pattern R030 reports.

At phone width, however, a 12rem reservation may leave too little space for the heading. Switch the control back into flow and stack it when the component becomes narrow. Do not remove the padding while leaving the absolutely positioned button behind, as the problem example did. The overlay design and the stacked design are separate states; each needs a complete set of rules.

An edge case: a badge intended to remain at the physical right edge in both directions should use right. Logical properties encode the relationship you mean, not an instruction that every right side must become a left side. Write down the intended edge before choosing the property.

Check which ancestor owns the position#

An absolute inset is measured against a containing block, commonly the nearest ancestor that establishes a positioning context. In the market example, position: relative on .market-header gives the filter a nearby reference. Remove that rule, and the button may be positioned against a more distant ancestor. Its right: 1rem remains perfectly valid CSS while referring to the wrong rectangle. The MDN positioning reference explains this relationship.

When a button appears far from its title, inspect the ancestor chain before changing the inset value. In DevTools, turn the ancestor's position: relative on and off to see which box governs the control. Then check whether the control was meant to be tied to that component, to a larger panel, or to the viewport. The right value of inset-inline-end cannot rescue a button attached to the wrong containing block.

The same exercise helps with responsive variants. A desktop wrapper may be the positioned ancestor, while a mobile redesign moves the header into a different wrapper. CSS selectors still match, but the coordinate system has changed. Document the intended anchor in the component styles, and test the element after it moves between page layouts.

Negative margins and transforms move paint toward neighbors#

A negative margin can pull a badge or action toward a nearby element. That may look tidy in an English header with spare width, then cross the Arabic title when the same row narrows. Inspect both the margin value and which side it acts on. A physical margin-left remains left in RTL; it does not become a start or end margin just because the page changed direction. The CSS box model guide explains how margins affect space between boxes.

If the margin represents separation at the inline start, use margin-inline-start. If it deliberately overlaps a thumbnail, test the overlap against the actual thumbnail and text at the narrowest width. A negative margin can be a design tool. It should not be the only thing preventing a button from appearing too far away in one language.

Transforms are different. transform changes the painted position without changing normal document flow. The layout still reserves the element's old position while the transformed element can cover a neighbor. translateX() also does not mirror itself for RTL. If a mobile rule shifts a chip or button horizontally, compare its untransformed layout box with the painted rectangle in DevTools. A control can have plenty of allocated space and still be painted on top of the heading.

Do not fix such a collision with a larger z-index, which only chooses the winner of the collision. Remove the transform if it was compensating for a layout gap, use grid or flex alignment for the gap, or define direction-specific movement if animation is genuinely required. Test both the resting and animated states. The first frame and final frame may each fit while the middle travels through a label.

Flex and grid can still run out of room#

Sometimes both elements remain in normal flow and still seem to collide. A long Arabic label can make one flex item resist shrinking while the button keeps its minimum width. The row needs more inline space than the component has. Flex items have an automatic minimum size related to their content; MDN notes that they do not shrink below their minimum content size by default.

Give the text item permission to shrink, then provide a way for its text and sibling to fit:

css
.market-header__copy {
  min-inline-size: 0;
}

.market-header__title {
  white-space: normal;
}

min-inline-size: 0 does not create extra room on its own. It lets the item use a smaller share of the row. If the title needs two lines, the header must have enough block space. If the button and title cannot both fit at a usable width, stack them. For grid, minmax(0, 1fr) similarly permits the flexible track to become narrower than its content-based minimum, but the text still needs a wrapping policy.

Avoid using flex-direction: row-reverse to make this “look RTL.” A normal row inside an RTL context already starts at the right; reversing it can create a visual order that disagrees with keyboard navigation. R081 reports that specific misuse when tab order fights visual order. Let document direction establish inline start, then keep source order meaningful as the layout stacks.

Fixed-pixel widths deserve a separate look. R043 reports a fixed-pixel-width container whose Arabic text exceeds the space. If a market title is forced into such a box, allowing it to shrink may only make the overflow more visible. Replace the rigid width or move the button to another row. A narrower box is not useful just because it is technically narrower.

The viewport breakpoint may miss a narrow component#

The market header could appear at full page width on the directory route and inside a sidebar on the map route. A max-width media query sees the viewport, so it may keep the two-column header while the sidebar is narrow. The overlap is local, even though the browser window is wide.

When the component's own available width determines whether the title and filter fit, a CSS container query can make that decision. Put container-type: inline-size on a wrapper around the header. The query then styles the header as a descendant:

css
.market-header-slot { container-type: inline-size; }

@container (inline-size < 36rem) {
  .market-header { grid-template-columns: minmax(0, 1fr); }
  .market-header__filter { justify-self: start; }
}

This assumes a .market-header-slot wrapper around the example header. The wrapper matters because a size query styles descendants based on the container; the header cannot query its own size and use the result to style itself. Choose a threshold by testing where the real title and control stop fitting, not by copying the width of a popular phone.

Container queries are not required for every mobile bug. A page-level navigation switch may genuinely depend on viewport width. Use the dimension that creates the collision. If a component can be narrow in more than one page layout, its container is usually the more useful measuring tape.

Fixed and sticky controls need a content allowance#

A bottom action bar may sit correctly at the viewport edge while covering the last market result. A sticky header may cover the first line after a jump link. This is still overlap, but the fix is not to reverse either component. Positioned controls can leave normal flow or remain pinned while content moves beneath them. The MDN position reference describes the different modes.

If a fixed bottom bar is part of the page, reserve enough block-end space in the scrolling content for the bar at its largest supported text size. Keep the bar's own height content-driven where possible. If the bar wraps into two lines in Arabic, a hard-coded allowance measured from the English version can fail even though the bar itself looks fine.

For a sticky header, test scrolling and anchored navigation, not just the top of the page. A heading can be present in the DOM and land underneath the sticky layer. Verify that focus and the scrolled target remain visible after the browser moves to it. That is an interaction issue a static screenshot will not find.

Fixed controls near screen edges may also need safe-area consideration on devices with cutouts. Safe-area values describe physical screen edges, so do not swap those physical insets merely because the content is RTL. The button's semantic placement may be logical; the device's cutout is still attached to its actual edge. Keep those two concerns separate.

Fonts and dynamic content can create a later collision#

The initial render may fit, then a webfont loads and the Arabic title takes another line. Or the filter label changes after a selection. A validation message appears below the title and expands the copy area. If the header's height is fixed, the new line may paint over the first market result. If the filter is absolute, it remains at its old coordinates while the text grows around it.

Test after fonts finish loading and again with the fallback face. R050 reports Arabic text falling back past the declared family, including a webfont that failed to load. A font report is not an overlap report, but different glyph metrics can reveal a layout that was balanced on a very small gap. The fix may involve both the font asset and a header that can grow.

Use the product's real states: long Arabic title, selected filters, empty state, error message and any badge added after interaction. Do not manufacture an English string by repeating a word and assume it predicts Arabic wrapping. Synthetic expansion is useful early; final content shows the actual break points.

If the collision is between two lines of text rather than two component boxes, inspect line height and any fixed block size. That is a different cause from an absolutely positioned button. R041 is about visible text-bearing elements overlapping; it does not explain whether the cause was font metrics, a transform or positioning. The diagnosis still belongs to you, regrettably.

A fixed header height deserves special suspicion here. The Arabic title can wrap correctly onto a second line, but a height chosen for one English line keeps the next market result at the old vertical position. The text and result then occupy the same space even though neither is absolutely positioned. Change the header to min-block-size if the design needs a minimum visual height, and let the block grow with the title and filter state. Test after a font change and at increased text size. If the header must remain bounded, the content needs a different layout or a deliberate local scroll region; simply clipping the second line does not restore the lost information.

Verify both geometry and interaction#

An overlap fix is complete when the text is readable, the control can be reached, and the layout remains stable through its width and state changes. A useful test pass is small but deliberate:

  1. Reproduce the collision at the exact failing width and one width on either side of the breakpoint.
  2. Inspect the two rectangles, their computed positioning and their nearest layout parent.
  3. Apply the smallest structural fix, then check the same Arabic text and control state.
  4. Repeat in LTR to confirm that the original wide and narrow layouts still make sense.
  5. Tab through the header and activate the filter. A visible button under a transparent layer is still unusable.
  6. Change the title or filter label to a longer real translation, then test font fallback and enlarged text.
  7. Scroll through fixed or sticky controls and check that the first and last content items remain visible.

Automated geometry can flag likely collisions by comparing rectangles, but it needs context. Text inside a badge intentionally intersects the badge's own rectangle. A shadow may paint beyond a box without blocking anything. Even two sibling rectangles can intersect because one contains transparent space. For the market header, assert the title and filter do not intersect at widths where they should be separate, then back the assertion with a visual and keyboard check.

If the button looks visible but does not respond to a pointer, another layer may be covering its hit area. At the button's centre, document.elementFromPoint() identifies the topmost element at that viewport position, ignoring layers with pointer-events: none. This console check is a lead for the market filter, not a full interaction test:

js
const filterButton = document.querySelector(".market-header__filter");
const rect = filterButton.getBoundingClientRect();
const hit = document.elementFromPoint(
  rect.left + rect.width / 2,
  rect.top + rect.height / 2
);
console.log(hit === filterButton || filterButton.contains(hit));

If it prints false, inspect the element returned in hit and its stacking context. A transparent overlay can block a button while leaving its pixels visible. If it prints true, test the edges and activate the control anyway; the centre point is only one point. Keyboard focus and activation also matter, because a pointer test cannot tell you whether the button makes sense in source order.

Do not equate lack of page overflow with lack of overlap. Two elements can cover each other entirely within the viewport, so R040 may have nothing to report. Conversely, a button that overhangs the page can create document-level horizontal overflow without touching the title. Name the failure you actually see and test that failure after the fix.

Give each element its own place#

The reliable mobile layout gives text and controls space in normal flow whenever both need to remain visible. Use logical positioning when an overlay is intentional, and move to a stacked layout when the available width runs out. Negative margins and transforms are not substitutes for a width budget. z-index is not a width budget either, despite its enthusiasm for large numbers.

A free Ritla scan can surface visible text-bearing elements that overlap and direction-sensitive physical CSS that deserves inspection on the Arabic page. Reproduce any finding at the narrow state where it appears, then use the RTL CSS guide if the collision comes from left/right rules that should follow inline start or end. The title and filter should each have room to do their job. A wrestling match is not a mobile layout.

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