Absolute positioning in RTL layouts

Learn how to anchor badges, menus and tooltips in RTL with logical insets, correct containing blocks, safe centering and overflow tests.

Guide14 min read

An absolutely positioned control can move to the wrong side in RTL for two entirely different reasons. It may be anchored with left or right when the design means start or end. Or it may be using the wrong containing block, so the offset is measured from a larger ancestor than anyone intended.

Fix only the first problem and the element becomes logically misplaced with modern CSS. Progress of a sort.

The reliable pattern has three parts:

  • establish the intended containing block;
  • choose logical insets when the edge follows writing direction;
  • keep physical offsets when the position is genuinely tied to the viewport, a coordinate system or a fixed side.

Then test the transform, available space and text length separately. Absolute positioning removes an element from normal flow. It does not reserve space, prevent overlap, mirror translateX() or decide where a wide menu should go when there is no room. The property has boundaries. We should respect them, if only because it will not respect ours.

Physical offsets and logical insets#

For horizontal English and Arabic interfaces, the common mappings are:

Physical propertyLogical meaning in LTRLogical meaning in RTL
leftInline startInline end
rightInline endInline start
topBlock startBlock start
bottomBlock endBlock end
inset-inline-startLeftRight
inset-inline-endRightLeft
inset-block-startTopTop
inset-block-endBottomBottom

The CSS Positioned Layout specification defines top, right, bottom and left as physical insets, and the inset-block-* and inset-inline-* properties as flow-relative insets. MDN's guide to logical positioning shows the same distinction in an absolute-positioning example.

The table is a result for writing-mode: horizontal-tb. Logical properties also respond to vertical writing modes. inset-inline-start can map to top or bottom there. Its definition is “inward from inline start,” not “the version of right we use for Arabic.”

Before replacing a physical inset, name the edge's job:

  • A card action at reading end needs inset-inline-end.
  • A status marker at reading start needs inset-inline-start.
  • A control at the top of a card needs inset-block-start.
  • A marker tied to the physical right side of a chart should keep right.
  • A centered tooltip may be better served by physical centering or auto margins, not a direction-dependent translation.

Logical CSS is not a campaign to remove the words left and right. It is a way to stop using them when they do not mean left and right.

Anchor a card action to inline end#

Consider an equipment reservation card with an actions button in its top trailing corner. In English that corner is top-right. In Arabic it should be top-left, leaving the right-hand reading start clear for the title.

Problem

html
<article class="device-card">
  <h2 class="device-card__title">جهاز قياس التنفس</h2>
  <p class="device-card__status">متاح في غرفة الأجهزة</p>
  <button class="device-card__actions" type="button" aria-label="خيارات الجهاز">
    ⋯
  </button>
</article>
css
.device-card {
  position: relative;
  padding: 1rem 3.5rem 1rem 1rem;
}

.device-card__actions {
  position: absolute;
  top: 1rem;
  right: 1rem;
}

The English layout reserves physical right padding and places the button on the physical right. Under RTL, the Arabic title starts on the right too. The button and the space reserved for it have not moved, so the card gives its most useful corner to two elements at once.

An RTL override can swap the sides:

css
[dir="rtl"] .device-card {
  padding-right: 1rem;
  padding-left: 3.5rem;
}

[dir="rtl"] .device-card__actions {
  right: auto;
  left: 1rem;
}

This can render correctly, but it duplicates the relationship across selectors. The logical version says the relationship once.

Better

html
<article class="device-card">
  <h2 class="device-card__title">جهاز قياس التنفس</h2>
  <p class="device-card__status">متاح في غرفة الأجهزة</p>
  <button class="device-card__actions" type="button" aria-label="خيارات الجهاز">
    ⋯
  </button>
</article>
css
.device-card {
  position: relative;
  padding-block: 1rem;
  padding-inline-start: 1rem;
  padding-inline-end: 3.5rem;
}

.device-card__actions {
  position: absolute;
  inset-block-start: 1rem;
  inset-inline-end: 1rem;
}

In LTR, inline end is right, so the result is unchanged. In RTL, inline end is left, and both the button and its reserved padding move there. The title can begin on the right without colliding with the control.

Changing only the inset would not be enough. If padding-right: 3.5rem remained, the button would move left while the empty space stayed right. RTL bugs often arrive in pairs because the original physical assumption was written in pairs.

Ritla check R030 names direction-sensitive physical properties without an RTL override. It does not decide that a particular right must become inset-inline-end; the design intent chooses start, end or a fixed physical side.

The containing block decides what the inset measures#

An inset is measured from the positioned element's containing block. For position: absolute, that is usually the padding box of the nearest ancestor that establishes an absolute-positioning containing block. A positioned ancestor, such as one with position: relative, is the familiar way to establish it.

Without this rule on the card:

css
.device-card {
  position: relative;
}

the actions button searches farther up the ancestor chain. If no qualifying ancestor exists, it can end up positioned against the initial containing block instead of the card. Replacing right with inset-inline-end will mirror the edge correctly and still measure it from the wrong rectangle.

The MDN guide to containing blocks lists the rules and the less obvious ancestors that can establish a containing block. Non-default transform, filter, perspective, contain and related properties can do it too. A wrapper added for animation may therefore change where a nested popup is positioned even if nobody touched the popup stylesheet.

When an element appears far from its trigger, inspect ancestors rather than increasing the offset until the screenshot behaves. In DevTools:

  1. Select the absolutely positioned element.
  2. Identify its containing block in the layout or computed-style tools.
  3. Check positioned ancestors and ancestors with transforms, containment or filters.
  4. Confirm the containing block's padding box is the rectangle the design intends.
  5. Only then inspect which inset edge is used.

An offset is not a coordinate in the page. It is a distance inward from a particular edge of a particular containing block. Losing track of either “particular” produces creative results.

Keep the positioning container unfragmented#

A positioned inline ancestor can establish a containing block, but an inline element may be split across lines and bidi runs. The positioned-layout specification defines one rectangle from those fragments. That rectangle can extend across several line fragments instead of matching the visible piece beside the popup.

For a menu attached to text or an inline trigger, give the anchor an unfragmented wrapper:

html
<span class="device-menu">
  <button type="button" aria-expanded="false">خيارات</button>
  <span class="device-menu__panel" hidden>إدارة الحجز</span>
</span>
css
.device-menu {
  position: relative;
  display: inline-block;
}

inline-block participates inline while creating one box for the anchor. A plain inline wrapper that wraps across lines is a poor reference rectangle for an overlay. This becomes especially difficult to reason about in bidirectional text, where visual fragments need not follow a simple left-to-right sequence.

Use a button or another real interactive element as the trigger, keep the popup adjacent in the DOM when practical, and let the wrapper establish geometry. Absolute positioning can arrange the visual layer. It should not be asked to invent the control's semantics.

Align dropdown edges with start or end#

A dropdown does not merely belong on “the other side in Arabic.” It usually aligns one of its edges with the corresponding edge of its trigger.

To align the popup's start edge with the trigger's start edge:

css
.device-menu__panel {
  position: absolute;
  inset-block-start: calc(100% + 0.5rem);
  inset-inline-start: 0;
  min-inline-size: 12rem;
}

In LTR, the left edges align and the popup extends right. In RTL, the right edges align and it extends left.

To align trailing edges instead, use inset-inline-end: 0:

css
.device-menu__panel {
  position: absolute;
  inset-block-start: calc(100% + 0.5rem);
  inset-inline-end: 0;
  min-inline-size: 12rem;
}

In LTR, the right edges align. In RTL, the left edges align. This often makes a wide menu extend inward when the trigger sits at the container's trailing edge, but it is not collision detection. A menu near a viewport boundary can still overflow if it is wider than the available space.

Test both trigger edges, narrow viewports, zoomed text and long Arabic labels. If the popup must choose among several placements based on available space, use a positioning system that measures collisions rather than accumulating locale selectors. Absolute insets express an anchor. They do not inspect the room and make executive decisions.

Logical insets use the positioned box's direction#

This detail is easy to miss: inset-inline-start and inset-inline-end map using the positioned element's own writing mode and direction. MDN's inset-inline-start reference documents that dependency, and the Positioned Layout specification defines the inset relative to the box's own writing mode.

Suppose an Arabic card contains an LTR diagnostics menu. If the menu itself has dir="ltr", then inset-inline-end: 0 on that same box maps to its LTR right side, even though the surrounding card's inline end is left.

Separate placement direction from content direction:

html
<span class="diagnostics-menu">
  <button type="button">التشخيص</button>
  <span class="diagnostics-menu__positioner" hidden>
    <span class="diagnostics-menu__panel" dir="ltr" lang="en">
      Device diagnostics
    </span>
  </span>
</span>
css
.diagnostics-menu {
  position: relative;
  display: inline-block;
}

.diagnostics-menu__positioner {
  position: absolute;
  inset-block-start: 100%;
  inset-inline-end: 0;
}

The positioner inherits RTL from the Arabic component, so its logical end is the component's left edge. The inner panel establishes LTR for its English text without changing the edge used to place the wrapper.

You do not need this extra wrapper when the popup content uses the same direction as its anchor. Add it when one box would otherwise be responsible for two different directional jobs. CSS allows that arrangement, but it does not send a congratulatory note.

Centering is not a start-or-end problem#

A tooltip centered above a trigger should remain centered in LTR and RTL. Converting only its physical inset to a logical one can break a centering formula.

This common pattern is physically symmetric:

css
.help-tooltip {
  position: absolute;
  bottom: calc(100% + 0.5rem);
  left: 50%;
  transform: translateX(-50%);
}

left: 50% puts the tooltip's left edge at the horizontal center of the containing block. translateX(-50%) moves it left by half of its own width. The pair centers it regardless of text direction because both operations use the same physical x-axis.

Changing only left to inset-inline-start is wrong:

css
.help-tooltip {
  inset-inline-start: 50%;
  transform: translateX(-50%);
}

In RTL, inline start maps to the right. The tooltip's right edge is placed at the midpoint, then the physical transform still moves the box left. The two halves of the centering calculation now disagree about which edge they started from.

For simple horizontal centering, keeping the physical left and translateX() pair is valid. Center has no directional mirror. You can also avoid the transform:

css
.help-tooltip {
  position: absolute;
  inset-block-end: calc(100% + 0.5rem);
  inset-inline: 0;
  inline-size: max-content;
  max-inline-size: min(20rem, 90vw);
  margin-inline: auto;
}

The two inline insets define the available width, and auto inline margins center a max-content box within it. Test this with your actual sizing and overflow rules. Tooltips have an unfortunate tendency to become small essays after localization.

translateX() itself is physical and does not mirror with dir. That is not a defect when it is part of a physical centering equation. It is a defect when a translation is meant to move toward logical start or end. Classify the motion before flipping its sign.

Two insets can position or stretch the box#

Setting both opposing insets can define the available size of an absolutely positioned box:

css
.inline-banner {
  position: absolute;
  inset-inline: 1rem;
  inset-block-start: 1rem;
}

With an automatic inline size, the banner stretches between inline start and inline end, leaving 1rem on both sides. The logical shorthand uses start then end, so a single value applies to both.

Add an explicit inline size and the axis can become over-constrained:

css
.inline-banner {
  position: absolute;
  inset-inline-start: 1rem;
  inset-inline-end: 1rem;
  inline-size: 18rem;
}

The box now has two insets and a size competing to determine one axis. Under the current Positioned Layout rules, the containing block's writing mode determines the weaker physical inset when that axis is over-constrained. When the box and its containing block share direction, the logical end-side inset loses: the right side is ignored in LTR and the left side is ignored in RTL. A nested box with a different direction is another reason not to use over-constraint as an alignment technique.

Avoid relying on this conflict as an alignment technique. Choose one of these clearer models:

  • set start and end insets, leave inline size automatic and let the box stretch;
  • set one logical inset and an inline size;
  • set both insets, a max size and auto margins when centering is intended;
  • use normal flow, Grid or Flexbox when the element should participate in layout.

The browser has deterministic rules for over-constraint. Your reviewer should not need to recite them to understand where a banner belongs.

inset is a physical shorthand#

The name inset sounds logical because the logical longhands also begin with inset-. The shorthand is not automatically logical.

css
.overlay {
  inset: 1rem 2rem 3rem 4rem;
}

Those values map like the physical margin shorthand: top, right, bottom, left. They do not mean block start, inline end, block end and inline start. The Positioned Layout specification acknowledges that this is confusing, which is kind of it.

Use the two-axis logical shorthands when direction matters:

css
.overlay {
  inset-block: 1rem 3rem;
  inset-inline: 4rem 2rem;
}

Here the first value in each shorthand is start and the second is end. A symmetric inset: 0 is safe across directions because every physical side receives the same value. A four-value inset with unequal horizontal offsets is physical and should be reviewed as such.

Do not mix logical and physical members of the same property group casually:

css
.overlay {
  right: 1rem;
  inset-inline-end: 0.75rem;
}

In LTR, both declarations map to the right inset and normal cascade order decides which wins. In RTL, inset-inline-end maps to left while right remains right, so both sides may become non-auto. The element can stretch, become over-constrained or move differently between directions. During migration, remove the old physical declaration instead of leaving it nearby as a historical exhibit.

Absolute elements do not reserve space#

An absolutely positioned box is removed from normal flow. Its parent does not become taller or wider to accommodate it, and siblings are laid out as if the box were not occupying their space. This is why a short English badge can sit politely in a corner while a longer Arabic label covers a heading.

Logical insets move the overlap to the correct side. They do not prevent it.

Use absolute positioning for overlays whose size is constrained and whose absence from flow is intentional: an icon button over a preview, a small status dot, a popup layer or a decorative mark. Reserve logical padding when nearby content must avoid the overlay, as the device-card example did.

If a label's translated length should affect card size, normal layout is usually the better tool:

css
.device-card__header {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: start;
  gap: 0.75rem;
}

The title and status can now size the header together. Grid follows direction, wraps content and keeps both elements in flow. Replacing absolute positioning is sometimes the best absolute-positioning fix. The property will recover.

Visible text-bearing elements overlapping are named by R041. If a fixed-pixel-width container makes Arabic text exceed the available space, R043 names that separate constraint. Neither check implies that logical insets alone will repair the component.

Fixed positioning can acquire a local containing block#

position: fixed normally uses the viewport as its containing block. Certain ancestor properties, including a non-none transform, can establish a containing block for fixed descendants. A fixed help panel inside an animated application shell may therefore behave like it is fixed to that shell rather than the viewport.

This problem is not specifically RTL, but RTL can reveal it when the shell's transformed edge and the viewport's logical edge differ. Inspect transformed, filtered and contained ancestors before changing a fixed element's insets.

When a panel should stay at viewport inline end, a logical inset expresses that relationship:

css
.support-panel {
  position: fixed;
  inset-block-start: 1rem;
  inset-inline-end: 1rem;
}

It appears top-right in LTR and top-left in RTL, provided the viewport is actually its containing block. If an ancestor captures it, the same declarations position it against that ancestor. Correct vocabulary cannot rescue the wrong rectangle.

If the panel should remain at the physical right in every locale, use right. A fixed emergency control, map legend or browser-like tool may have a physical placement requirement. Document that exception so a later logical-property migration does not “repair” it.

Watch overflow on the RTL side#

An anchored popup can extend beyond its containing block or viewport even when its inset is logically correct. Width, transforms, negative margins and an unexpectedly distant containing block can all create overhang.

In Chromium 151, checked on 2026-09-24, content extending past the left edge of an RTL page created horizontal scroll by the amount of the overhang, while content past the right edge did not. Firefox and Safari were not measured for that fixture. The practical lesson is to inspect both physical edges rather than assuming the scrollable side matches LTR.

For menus, tooltips and badges:

  • test triggers near both viewport edges;
  • test the widest supported localized content;
  • check narrow viewports and enlarged text;
  • inspect the element's bounding rectangle and the document's scroll width;
  • remove transforms temporarily to separate anchoring from motion;
  • verify hidden popups do not affect the measurement method;
  • test after dynamic content is inserted, not only with the initial fixture.

Do not apply overflow-x: hidden to the page as the first repair. It can hide a menu, focus ring or content that users still need. Find the box that extends beyond its intended boundary and correct its anchor, width or collision behavior. The horizontal overflow guide covers that investigation in depth.

Ritla check R040 names document-level horizontal overflow. It reports the page-level result, not whether an absolute child, transform or fixed width caused it.

Audit physical positioning without blind replacement#

A stylesheet search for physical offsets is a good start:

text
left:
right:
top:
bottom:
inset:
translateX(

The result is an audit list, not a replacement queue. Classify each use:

Existing codeIntended relationshipLikely action
leftReading startReplace with inset-inline-start
rightReading endReplace with inset-inline-end
topBlock startReplace with inset-block-start if writing modes matter
left or rightFixed physical coordinateKeep and document it
left: 50% plus translateX(-50%)Physical centeringUsually keep the pair
Both inline sides plus auto sizeStretch between edgesUse inset-inline
Text label overlaying variable contentShould participate in layoutReplace absolute positioning

When converting, inspect companion declarations in the same component: padding that reserves space, border radii, transform origins, shadows, animations and RTL overrides. Changing the inset alone can leave the component half mirrored.

Also search CSS-in-JS objects, utility classes, component props and animation keyframes. A physical offset introduced by a design token is still physical. It merely arrived with documentation.

After the logical rule is in place, remove obsolete [dir="rtl"] swaps. Keep a direction-specific override only when the design really differs, not when the base declaration can express the relationship itself.

Test the anchor, not just the side#

Use the same component markup in LTR and RTL and verify these cases:

CaseWhat to check
Top-end actionRight in LTR, left in RTL, with matching reserved padding
Top-start markerLeft in LTR, right in RTL
Start-aligned menuCorresponding trigger and menu start edges align
End-aligned menuCorresponding trigger and menu end edges align
Centered tooltipCenter remains unchanged in both directions
LTR popup inside RTL UIPlacement wrapper follows RTL; inner text remains LTR
Fixed physical markerSame physical side in both directions
Long Arabic labelNo overlap, clipping or document overflow

For each case, inspect three rectangles: the positioned element, its containing block and the viewport. Confirm the inset is measured from the intended one. Then inspect computed direction, the winning inset declarations and any transform matrix.

Toggle dir on the component boundary rather than merely swapping strings. A logical inset responds to the positioned box's direction; translated text alone does not create that mapping. Test nested direction changes deliberately, especially when a popup contains English diagnostics, codes or other LTR material.

Use keyboard navigation too. Absolute positioning does not change DOM order. A visually top-left action can still receive focus after content much farther down the card if that is where it appears in the source. Decide whether that order is meaningful and visible at every breakpoint.

Finally, test with the popup open. A closed menu tells you very little about menu positioning, which sounds obvious because it is obvious. Obvious things remain highly skilled at escaping test plans.

Put the element relative to the meaning#

RTL-safe absolute positioning is not a matter of replacing every left with right. Establish the correct containing block, express start and end with logical insets, and keep physical coordinates where the design genuinely depends on physical space. Treat centering and transforms as separate calculations, and let variable text participate in normal layout when it needs to reserve room.

A free Ritla scan can surface direction-sensitive physical properties without an RTL override R030, visible text-bearing elements overlapping R041 and document-level horizontal overflow R040. The intended anchor edge and containing block still need the component-level tests above.

If the absolute element is fixed but the surrounding component still contains physical margins, padding or borders, continue with the RTL CSS guide. A logical inset is useful. Knowing what it is inset from is the part that finishes the job.

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.