Fixing Mirrored Icons and Logical CSS Properties in RTL
In a right-to-left layout, a back arrow keeps pointing left, a margin-left leaks an asymmetric gap onto the wrong side, and a transform: scaleX(-1) accidentally mirrors a clock or a brand logo that should never flip. These bugs share one root cause: the layout still thinks in physical left/right instead of flow-relative start/end, and the icon-flip set was chosen by guesswork instead of by directional meaning. This page shows exactly which icons must mirror, which must not, how to flip them with [dir="rtl"] and :dir(rtl) selectors, and how logical properties (margin-inline-start, padding-inline, inset-inline) make the whole layout direction-agnostic.
The symptom you recognise: you set <html dir="rtl">, text reverses correctly, but the UI is subtly broken — the “next” chevron still points the wrong way, the close button sits on the wrong corner, and one stubborn 24px gap refuses to move. The flip is not automatic. Browsers mirror text and inline flow, but <img>, inline SVG, and any physical-direction CSS stay exactly where you put them. This is the icon-and-spacing layer of RTL & Bidirectional Layout Engineering, narrowed to the two problems that produce the most “it looks fine in English” regressions.
Root cause analysis
Two independent mechanisms fail together, which is why the bug feels mysterious.
The browser only mirrors what flows, not what you draw. Setting dir="rtl" reverses the inline base direction per the Unicode Bidirectional Algorithm (UAX #9), so text and inline-level boxes reorder. But a graphic — an <img src="arrow.svg">, an inline SVG element, an icon font glyph, a CSS background-image — is opaque content. The browser has no idea a path inside it points left. It will not flip it. So every directional glyph stays frozen in its left-to-right orientation while the layout around it reverses, producing arrows that point “backwards” relative to the new reading order.
Physical CSS properties don’t know about direction at all. margin-left, padding-right, left, right, text-align: left, and border-left are anchored to the physical viewport edges. They do not respond to dir. So a card built with margin-left: 16px keeps its gap on the geometric left in both directions — which is the start side in LTR but the end side in RTL. That single asymmetric value is the “leaking margin” you keep chasing. The fix is not a second RTL override; it is replacing the physical property with its flow-relative logical equivalent (margin-inline-start), which the engine resolves against the current writing mode automatically. Getting dir itself resolved correctly upstream is a locale negotiation concern; this page assumes dir is already correct and focuses on what the CSS and icons do with it.
The flip set is a semantic decision, not a blanket transform. The frequent over-correction is [dir="rtl"] svg { transform: scaleX(-1); }, which mirrors every icon — including logos, clocks, checkmarks, the play triangle (media controls keep their LTR direction by convention), and the magnifying glass. Per the CLDR mirroring guidelines, only icons that convey direction or temporal/sequential order should mirror. A blanket selector is always wrong; the flip must be opt-in per icon.
Minimal reproducible example
A toolbar that looks correct in English and breaks in Arabic:
<html dir="rtl" lang="ar">
<style>
.btn { margin-left: 12px; } /* leaks to the wrong side in RTL */
.icon-back { /* no flip rule at all */ }
</style>
<button class="btn">
<svg class="icon-back" viewBox="0 0 24 24" width="20">
<path d="M15 6l-6 6 6 6" fill="none" stroke="currentColor" stroke-width="2"/>
</svg>
رجوع <!-- "Back" -->
</button>
Two defects: the back chevron still points left (in RTL, “back” should point right, toward where the reader came from), and the 12px gap sits on the physical left — the end side in RTL — so it appears after the button instead of before it. The text reverses correctly, masking the problem until a designer notices the arrow “feels wrong”.
The fix, annotated
Replace physical spacing with logical properties and flip only the directional icons, scoped by selector:
/* 1. Logical spacing: resolves to left in LTR, right in RTL automatically. */
.btn {
margin-inline-start: 12px; /* never margin-left again */
padding-inline: 8px 16px; /* start=8, end=16; mirrors with direction */
}
/* 2. Flip ONLY directional icons. :dir(rtl) matches the element's
resolved direction; [dir="rtl"] is the wider-supported fallback. */
[dir="rtl"] .icon-back,
[dir="rtl"] .icon-forward,
[dir="rtl"] .icon-chevron,
[dir="rtl"] .icon-send,
[dir="rtl"] .icon-undo {
transform: scaleX(-1); /* horizontal mirror, vertical untouched */
}
/* 3. Hard opt-out: anything tagged must survive a stray blanket rule. */
[dir="rtl"] .no-flip { transform: none !important; }
<!-- Mark each icon by intent, not by appearance. -->
<svg class="icon-back" viewBox="0 0 24 24"></svg> <!-- flips -->
<svg class="icon-logo no-flip" viewBox="0 0 24 24"></svg> <!-- never flips -->
<svg class="icon-clock no-flip" viewBox="0 0 24 24"></svg> <!-- real object, no flip -->
Annotations on the non-obvious lines:
margin-inline-startis resolved against the element’sdirection/writing-mode, so one declaration covers both LTR and RTL — there is no second[dir="rtl"]override to maintain. The same logic is why you should also dropleft/rightforinset-inline-start/inset-inline-endon absolutely-positioned close buttons.:dir(rtl)reads the resolved direction even for nesteddir="auto"subtrees, which[dir="rtl"]cannot; pair them so older engines still get the attribute-selector path. This matters most for user-generated bidi content, where direction is detected per-element rather than set globally.scaleX(-1)mirrors horizontally only. If an icon is itself asymmetric top-to-bottom (a “reply” curve), verify it visually — mirroring can produce a valid but unintended glyph, in which case ship a dedicated RTL asset instead of a transform.- The
.no-flip { transform: none !important; }rule is insurance: if any third-party stylesheet ships a blanket[dir=rtl] svgflip, your protected icons still render correctly. Spacing that mirrors here is the same discipline that keeps a date and number format aligned to the correct edge in a mixed-direction line.
For icons that genuinely need a different shape rather than a mirror (a localized “send” paper-plane, say), swap the asset by direction instead of transforming:
.icon-send { background-image: url(/icons/send-ltr.svg); }
[dir="rtl"] .icon-send { background-image: url(/icons/send-rtl.svg); }
Verification snippet
Assert the resolved spacing and the icon transform programmatically so a regression can’t slip back in. This runs in any DOM test environment (Vitest + jsdom, Playwright, Cypress):
// Set up an RTL container and read computed styles.
document.documentElement.setAttribute('dir', 'rtl');
const btn = document.querySelector('.btn');
const back = document.querySelector('.icon-back');
const logo = document.querySelector('.icon-logo');
const cs = getComputedStyle(btn);
// In RTL, margin-inline-start must land on the RIGHT physical side:
console.assert(cs.marginRight === '12px', 'logical margin must mirror to the end side');
console.assert(cs.marginLeft === '0px', 'physical left must stay clear');
// Directional icon is mirrored; the logo is not:
console.assert(getComputedStyle(back).transform === 'matrix(-1, 0, 0, 1, 0, 0)', 'back must flip');
console.assert(getComputedStyle(logo).transform === 'none', 'logo must never flip');
A faster static gate catches the root cause before it ships — a Stylelint rule that bans physical properties outright and forces logical equivalents in component styles:
{
"rules": {
"csstools/use-logical": ["always", {
"except": ["float"]
}]
}
}
With stylelint-use-logical enabled, margin-left, padding-right, text-align: left, and inset shorthands fail CI, pushing the whole codebase toward direction-agnostic CSS so RTL works without per-rule overrides.
When to escalate
If icons and spacing are correct but the layout still breaks — numbers running backwards inside a sentence, a Latin product code splitting in the middle of an Arabic paragraph, or parentheses pairing to the wrong glyph — the problem is no longer CSS. It has moved into the bidirectional algorithm itself: isolation, embedding levels, and the dir="auto" / Unicode control characters (U+2066–U+2069) that govern how mixed-direction runs are ordered. Those are governed by UAX #9 and need <bdi>, unicode-bidi: isolate, or explicit isolates rather than logical properties. When a fix at the icon-and-spacing layer is not enough, step back up to the broader RTL & Bidirectional Layout Engineering cluster, which covers bidi isolation, mixed-direction forms, and the full mirroring matrix.
FAQ
Which icons should I mirror in RTL and which should I leave alone?
Mirror icons that encode direction or sequence: arrows, chevrons, back/forward, undo/redo, send, indent, and progress trackers. Do not mirror icons that represent real-world objects or universal symbols: logos, clocks, the media play triangle, checkmarks, the magnifying glass, phone, and anything containing numbers. The test is meaning, not shape — if the icon points the reader somewhere, flip it; if it depicts a thing, keep it.
Why does my margin-left still appear on the wrong side after setting dir="rtl"?
Because margin-left is a physical property anchored to the viewport’s left edge, and dir does not affect it. Replace it with the logical property margin-inline-start, which the browser resolves to the left in LTR and the right in RTL automatically. The same applies to padding, border, text-align, and positioning offsets — use the -inline-start/-inline-end logical variants throughout.
What is the difference between [dir="rtl"] and :dir(rtl)?
[dir="rtl"] is an attribute selector that matches only when the literal dir="rtl" attribute is present on (or inherited via) an element. :dir(rtl) is a pseudo-class that matches the element’s resolved direction, including subtrees set to dir="auto" where direction is detected from content. Use :dir(rtl) for accuracy with user-generated bidi text, and keep [dir="rtl"] as a fallback for engines without :dir() support.
Related
- RTL & Bidirectional Layout Engineering — the full cluster on bidi isolation, mirrored layouts, and mixed-direction forms.
- Locale Negotiation Strategies — how the
dirvalue is resolved from the negotiated locale before any CSS runs. - Date & Number Formatting Standards — keeping numerals and dates aligned to the correct edge in RTL lines.
- Setting Up Graceful Fallback Chains for Missing Strings — the same opt-in discipline applied to missing translations.