How to convert LTR CSS to RTL

Convert an LTR stylesheet to RTL without breaking English layouts by auditing physical assumptions, using logical CSS and testing exceptions.

Guide16 min read

Converting LTR CSS to RTL is not a matter of swapping every left for right and going home early. Some declarations should follow text direction, some should remain physically fixed, and some have no logical equivalent at all. A blind swap gets two of those groups wrong. This is considered a poor opening result.

The safer method is incremental:

  1. set direction in the markup;
  2. record the working LTR layout before changing it;
  3. inventory physical assumptions rather than replacing them immediately;
  4. convert flow-relative relationships to logical CSS;
  5. add narrow RTL rules for transforms, shadows, icons and other exceptions;
  6. test both directions at every component state and breakpoint.

The aim is one source of truth where the browser can map start and end for you. A separate RTL stylesheet can still be useful for a legacy codebase or third-party CSS, but it should be a deliberate build output, not a second handwritten account of the entire interface.

Establish direction before touching layout#

An Arabic document needs its language and base direction in HTML:

html
<html lang="ar" dir="rtl">

lang="ar" identifies the language. It does not set layout direction. The dir HTML attribute establishes the base direction and lets descendants inherit it. Ritla check R001 reports when document direction is missing, contradicted or declared only in CSS.

Do not start the conversion by putting this in a stylesheet:

css
html {
  direction: rtl;
}

It can make the page display right to left, but it leaves direction out of the document semantics. It also creates a selector trap: in Chromium 151, CSS direction: rtl without a dir attribute does not make :dir(rtl) match the element. Put direction in markup and let CSS respond to it. The HTML direction guide covers route-level direction, nested language runs and dir="auto" in more depth.

For separate English and Arabic routes, render the correct root attributes in the initial document. Changing them after the app mounts can briefly show the LTR layout, then move navigation, overlays and text after the first paint. That flash is not a CSS mystery. It is a timing decision wearing a stylesheet's coat.

If one route contains a nested component with a different base direction, put dir on that component boundary. Logical properties map from the styled element's computed direction, not from a permanent global notion of “the Arabic side.” This matters later when an English report, code editor or embedded vendor widget sits inside the Arabic page.

Freeze the LTR baseline#

Before changing declarations, record what must remain unchanged. A logical-property conversion is successful only if it fixes RTL and preserves the current LTR result. Otherwise the migration has quietly become a redesign, and the review will spend its afternoon deciding which changes were intentional.

Choose representative routes and capture them at the widths your product supports. Include states that ordinary screenshots omit:

  • navigation open and closed;
  • menus and popovers visible;
  • selected, disabled, invalid and loading controls;
  • long labels and two-line buttons;
  • empty results and dense results;
  • sticky headers after scrolling;
  • dialogs at phone and desktop widths;
  • hover and focus states where they alter borders, shadows or transforms.

Record more than pixels. Tab through interactive controls and note the expected focus sequence. Save the current horizontal scroll width. Identify the containing block for positioned elements. If the interface has animation, record the direction in which it enters, exits and nudges on hover.

Separate known LTR defects from conversion work. A clipped English label should not become an RTL regression merely because Arabic exposes it more dramatically. Fix it, track it or accept it, but name it before comparing the two directions.

Build a conversion inventory#

Search the source styles, generated styles and component APIs for physical assumptions. Start with terms such as:

text
left
right
margin
padding
border
radius
float
clear
text-align
box-shadow
translateX
background-position
transform-origin
clip-path

Include utility classes such as .ml-*, props such as align="right", Sass mixins, CSS-in-JS objects, design tokens and SVG files. Searching only .css files finds the assumptions that had the courtesy to announce themselves.

Do not make replacements from the search results. Classify each occurrence first:

CategoryExample intentConversion
Flow-relativeSpace before a labelUse inline or block logical CSS
Already direction-awareNormal flex rowLet inherited direction map it
SymmetricEqual padding on both inline sidesKeep the shorthand
Physically fixedMarker on the west side of a mapKeep the physical declaration
Directional exceptionArrow nudge or panel translationAdd a scoped RTL value
Content problemPhone, email or mixed-direction identifierFix markup and bidi handling, not box layout

The distinction between flow-relative and physical is the main judgment in the migration. “Left in the English screenshot” is not enough information. Ask why it is left.

A useful inventory row contains the component, declaration, intended relationship, planned replacement and test state. For example:

ComponentExisting ruleMeaningPlanned changeTest
Rehearsal cardborder-leftReading-start accentborder-inline-startLTR and RTL selected state
Stage mapleftWest coordinateKeep leftBoth locale routes
Next-scene linkpositive translateXMove toward inline endDirection variableHover and reduced motion
Floating notefloat: rightInline endReplace layout or use logical floatLong Arabic copy

That ledger prevents a codebase-wide replacement from erasing intent. It also gives reviewers something better than a diff containing two thousand changed property names and a hopeful description.

Convert one complete component first#

Choose a component that contains spacing, a border and a positioned control. It will expose the conversion pattern without involving the entire application shell.

Consider a theatre scheduling card. In English, it has a start-side accent, extra end padding for an action button, an indent from the start of its list and rounded corners on the accent side.

Problem

html
<article class="rehearsal-card">
  <p class="rehearsal-card__label">بروفة تقنية</p>
  <h2>مراجعة إضاءة المسرح الصغير</h2>
  <button class="rehearsal-card__action" type="button">
    فتح التفاصيل
  </button>
</article>
css
.rehearsal-card {
  position: relative;
  margin-left: 1.5rem;
  padding: 1rem 3.75rem 1rem 1rem;
  border-left: 0.25rem solid #7f56d9;
  border-radius: 0.75rem 0 0 0.75rem;
}

.rehearsal-card__action {
  position: absolute;
  top: 1rem;
  right: 1rem;
}

Setting the page to RTL changes text flow, but those declarations keep their physical meanings. The accent and indentation remain left even though reading start is now right. The action stays right, where the Arabic heading also begins. The rounded corners keep advertising the former start edge with admirable consistency.

An RTL override can swap each property, but a complete override would need to change the margin, padding, border, radii and positioned action together. Miss one and the card becomes half mirrored. Half-mirrored components are useful mainly as evidence that the stylesheet has several authors.

Better

css
.rehearsal-card {
  position: relative;
  margin-inline-start: 1.5rem;
  padding-block: 1rem;
  padding-inline-start: 1rem;
  padding-inline-end: 3.75rem;
  border-inline-start: 0.25rem solid #7f56d9;
  border-start-start-radius: 0.75rem;
  border-end-start-radius: 0.75rem;
}

.rehearsal-card__action {
  position: absolute;
  inset-block-start: 1rem;
  inset-inline-end: 1rem;
}

In LTR, inline start maps left and inline end maps right, so the English result stays where it was. In RTL, the mapping reverses. The margin, accent and start-side corners move right; the reserved padding and action move left.

The CSS Logical Properties specification defines these flow-relative mappings from the element's writing mode and direction. MDN provides side-by-side mappings for logical margin, border and padding properties and logical positioning.

Convert relationships, not isolated declarations. If right: 1rem moves to inset-inline-end, the padding that reserves room for that action must move to padding-inline-end in the same change. If a start border becomes logical, inspect its companion padding and radii. Directional assumptions travel in groups because designers, quite reasonably, compose whole components rather than disconnected CSS lines.

Ritla check R030 names direction-sensitive physical properties without an RTL override. Use the report as an inventory prompt. It cannot decide whether a given left edge means inline start or the literal left side of a stage map; that decision belongs to the component.

Use a mapping table, not memory#

For horizontal English and Arabic interfaces, these replacements preserve the LTR result while allowing RTL to mirror:

Physical LTR declarationLogical declaration when the meaning follows flow
margin-leftmargin-inline-start
margin-rightmargin-inline-end
padding-leftpadding-inline-start
padding-rightpadding-inline-end
border-leftborder-inline-start
border-rightborder-inline-end
leftinset-inline-start
rightinset-inline-end
widthinline-size, when the dimension follows the inline axis
heightblock-size, when the dimension follows the block axis
text-align: lefttext-align: start
text-align: righttext-align: end
float: leftfloat: inline-start
float: rightfloat: inline-end

This is not a universal search-and-replace table. It assumes that each physical side was standing in for that logical meaning. A button intentionally fixed to the physical right edge should keep right. A square with a fixed width does not gain anything from becoming inline-size. The table translates intent after you have identified it.

Four-value shorthands need care. This LTR declaration uses physical top, right, bottom and left order:

css
.panel {
  padding: 0.75rem 2rem 1rem 1.25rem;
}

There is no signal inside it saying whether the unequal values express physical geometry or content flow. Expand it before converting:

css
.panel {
  padding-block-start: 0.75rem;
  padding-inline-end: 2rem;
  padding-block-end: 1rem;
  padding-inline-start: 1.25rem;
}

Two-value symmetric shorthands often need no change. padding: 1rem 1.5rem already gives equal inline padding on left and right. Converting it may clarify intent, but it does not fix an RTL defect.

For asymmetric corner radii, use logical corner longhands. The first direction word identifies the block side and the second identifies the inline side. border-start-start-radius is block start plus inline start; in horizontal text it maps to top-left in LTR and top-right in RTL. Equal border-radius values are symmetric and can stay as they are.

Let flexbox and grid do their existing work#

Do not mirror a flex or grid layout before checking what direction already changes. In a dir="rtl" page, a normal flex row, grid auto-placement and table row begin on the right. That behavior was measured in Chromium 151 for this brief and follows the writing-mode concepts used by these layout systems.

This navigation row needs no RTL-specific reversal:

css
.schedule-tabs {
  display: flex;
  gap: 0.75rem;
}

The row flex direction follows the inline direction. Adding flex-direction: row-reverse for Arabic reverses a row that the browser has already started from the right. The first item then appears on the left, and keyboard focus still follows source order. R081 reports a flex row-reverse used to fake RTL when tab order fights the visual order.

Keep the DOM in the sequence users should read and operate. Let dir choose which physical side is inline start. Use gap for space between flex and grid items instead of directional margins where possible; a gap belongs between items and does not need mirroring.

Grid requires the same restraint. Auto-placement responds to direction, but authored track geometry, named areas and explicit line placement still deserve inspection. A first column with a fixed width may move with the grid's start edge; a component pinned to a numbered or named region may not represent the relationship you intended. Test the actual grid rather than adding a blanket transform or reversing its children.

Avoid order as an RTL patch. It changes visual placement, not document source, speech order or tab order. MDN's guide to ordering flex items explains that distinction. If Arabic needs a different semantic order, change the data or markup deliberately. If it needs the same semantic order from the opposite edge, normal direction already provides it.

Convert alignment and floats with context#

For text that should align with reading start, replace physical alignment:

css
.rehearsal-card {
  text-align: start;
}

text-align: start maps to left in LTR and right in RTL. end does the reverse. Centered and justified text are not left/right swaps, so leave them unless the Arabic typography needs a separate decision. Ritla check R022 reports text-align: left explicitly applied to majority-Arabic text.

Floats have logical values too:

css
.production-note__thumbnail {
  float: inline-start;
  margin-inline-end: 1rem;
}

The CSS Logical Properties specification defines inline-start and inline-end values for float and clear. Check them against your browser support policy. For interface layout, replacing a float with flex or grid is often the cleaner migration because those systems provide gaps, alignment and wrapping without text flowing around a box. Keep floats where text genuinely should wrap around media. R031 names float: left or float: right on content elements in RTL without an override.

Do not change data alignment by category alone. A column containing Arabic names should normally align to start. A column containing a fixed-format LTR identifier may need its own direction and alignment contract. That is a content-direction issue, not permission to force the whole table LTR. The bidirectional text guide covers isolation and mixed-direction values without turning this migration into a second article.

Keep physical CSS when the design is physical#

The purpose of logical CSS is accuracy, not ideological purity. A design still contains physical relationships.

Keep physical declarations for cases such as:

  • coordinates on maps, diagrams and seating plans;
  • a control tied to a physical viewport edge;
  • a crop or overlay aligned to a fixed part of an image;
  • light and depth effects whose source remains in one physical position;
  • centering based on a physical axis;
  • brand artwork that must not mirror;
  • third-party content with a documented LTR coordinate system.

For example, a marker placed on the left side of a theatre seating diagram should not move merely because the surrounding labels become Arabic:

css
.stage-map__west-marker {
  position: absolute;
  left: 1rem;
  top: 40%;
}

Document why it stays physical. Otherwise a later audit will see left, assume unfinished conversion and helpfully move the west marker east. A token called --space-inline-start should express flow; a prop called side="left" remains appropriate for a physical map annotation. Do not make every API logical. Make each API honest.

Handle transforms, shadows and icons as exceptions#

Logical properties cover the box model, sizing, alignment, insets and related values. They do not turn Cartesian effects into flow-relative effects. translateX(), a shadow's x-offset, background positions, gradients, clip paths and SVG coordinates remain physical.

For a small directional hover movement, keep the direction-dependent value explicit:

css
.next-scene-link {
  --inline-nudge: 0.25rem;
}

.next-scene-link:dir(rtl) {
  --inline-nudge: -0.25rem;
}

.next-scene-link:hover .next-scene-link__icon {
  transform: translateX(var(--inline-nudge));
}

The translateX() function translates on the physical x-axis. Its sign does not respond to dir. The :dir() pseudo-class selects by document directionality, which is another reason to establish dir in markup rather than CSS alone.

Use the same pattern when motion means toward inline start or end. Keep physical motion unchanged for a camera movement, chart axis or other physical coordinate. R033 is specific to a translateX that moves an element off-screen in RTL; smaller motion that points the wrong way still needs manual review.

A box-shadow has physical x and y offsets. MDN's box-shadow reference defines the first two lengths as horizontal and vertical offsets. Do not flip every shadow. A consistent light source should usually remain physically consistent. Flip only a shadow whose offset conveys start or end, such as a directional edge treatment. R032 reports direction-sensitive shadow x-offsets without an RTL counterpart.

Mirror icons by meaning, not by visual asymmetry. Back, forward, undo and redo icons often carry direction. Search, play, download, clocks, logos and many transport or safety symbols do not become their opposites in Arabic. Use a separate RTL asset or apply a scoped transform to the directional icon. Then verify its control still advances in the direction the icon indicates; R025 names a directional icon that points against that sequence.

Avoid transform: scaleX(-1) on an entire component. It mirrors text, images, focus rings and coordinate systems along with the icon you meant to fix. Apply it to the path or icon wrapper, and exclude marks whose shape must remain unchanged. Mirroring the whole card is the CSS equivalent of turning the paper around because one arrow is inconvenient.

Use direction overrides only for real exceptions#

Once flow-relative declarations are logical, most components should not need [dir="rtl"] duplicates. Keep direction selectors for effects that lack a logical form or for genuinely different product choices.

Prefer selectors that survive nested direction:

css
.next-scene-link:dir(rtl) {
  --inline-nudge: -0.25rem;
}

An ancestor selector is less precise:

css
[dir="rtl"] .next-scene-link {
  --inline-nudge: -0.25rem;
}

It matches any descendant of an RTL boundary, including a nested component that has reset itself to dir="ltr". :dir(rtl) asks about the element's own directionality. Use [dir="rtl"] when you intentionally need the explicit attribute boundary, such as a root theme rule. Use :dir(rtl) when the element's inherited or declared direction controls the style.

Keep overrides beside the base component or in a named cascade layer. A distant RTL file can work, but it makes every refactor a scavenger hunt: change the base rule, remember the mirror, discover the mirror has higher specificity, make tea.

During migration, physical and logical declarations can collide. They join the cascade after the browser maps the logical property to a physical side. This rule produces different conflicts by direction:

css
.note {
  margin-inline-start: 1rem;
  margin-left: 0;
}

In LTR, both declarations target the left margin and the later physical declaration wins. In RTL, inline start maps right, so the left reset no longer cancels it. Remove obsolete physical declarations rather than assuming the logical one wins because it sounds newer.

Inspect later shorthands too. A later margin: 0, padding: 0 or border: 0 can reset the physical side to which an earlier logical property maps. Specificity, source order and cascade layers still apply. Logical CSS has not negotiated special privileges.

Decide whether to generate an RTL stylesheet#

There are two viable delivery models.

One direction-aware stylesheet uses logical properties for shared rules and narrow direction selectors for exceptions. It keeps LTR and RTL behavior close together and lets components respond to nested direction. Prefer this for code you control when your browser support policy allows the logical properties you need.

Generated RTL CSS takes a physical LTR source and emits a mirrored bundle. This can be practical for a legacy stylesheet, a framework that already uses that build, or a product that ships separate locale assets. RTLCSS, for example, provides a command-line conversion from a source file to an RTL output file and supports directives for exceptions.

Generation is a transformation, not a design review. A tool can swap left and right; it cannot know whether left means reading start, west on a map or a fixed light source. Gradients, transforms, shorthand values and custom syntax may need configuration or an ignore directive.

If you generate an RTL bundle:

  • keep the LTR source authoritative and generate RTL in the build;
  • review the first output by component category;
  • mark physical exceptions in source with documented directives;
  • diff generated CSS when the source or converter version changes;
  • run both visual tests from built assets, not only development styles.

Do not maintain two handwritten full stylesheets unless the interfaces are genuinely separate designs. A bug fix copied between them will eventually miss one side. This is not cynicism. It is arithmetic applied to duplicated code.

A hybrid approach often works during migration: retain the generated RTL bundle for untouched legacy areas, convert active components to logical source rules, and remove their generated overrides as each component moves. Define the boundary clearly so a component is not being mirrored twice.

Migrate by component and delete old fixes#

Avoid a single repository-wide conversion commit. Change one component family at a time, run both directions, and keep the diff small enough to review.

A practical order is:

  1. shared tokens and root direction;
  2. spacing, borders and text alignment;
  3. flex and grid containers;
  4. positioned controls and overlays;
  5. directional icons and motion;
  6. shadows, backgrounds and generated content;
  7. legacy floats and third-party overrides;
  8. responsive and state-specific rules.

Within each component, convert the base rule and all its states together. A card may be logical at rest and revert to border-left when selected. A menu may use inset-inline-end on desktop and right inside a phone media query. The component is finished only when its branches agree on the same direction contract.

After verification, remove old RTL swaps that duplicate the new logical behavior:

css
/* Remove after the base rule uses padding-inline-start. */
[dir="rtl"] .rehearsal-card {
  padding-right: 1rem;
  padding-left: 3.75rem;
}

Do not delete an entire override block without inspecting it. The block may also contain a font, line height or state correction unrelated to side swapping. Remove declarations by responsibility, not by the enthusiasm of the cleanup ticket.

Add a lint rule or review check for new direction-sensitive physical declarations if that fits your toolchain. Allow documented physical exceptions. A rule that bans every left and right merely drives valid physical geometry into less obvious code.

Test both directions as one feature#

Run the same component fixture in LTR and RTL. Do not compare unrelated English and Arabic pages whose data, permissions or experiments differ. The direction change should be the main variable.

For each migrated component, verify:

AreaLTR expectationRTL expectation
Text startLeftRight
Start spacing and borderLeft sideRight side
End actionRight sideLeft side
Normal flex rowStarts leftStarts right
Source and focus orderMeaningfulSame meaningful sequence
Directional iconMatches LTR actionMatches RTL action
Physical map markerFixed sideSame fixed side
Hover motionIntended directionMirrored only if meaning requires it

Inspect computed direction and writing-mode, then inspect the winning declaration for each suspicious side. DevTools may show both the logical declaration and its physical computed result. If a logical property appears crossed out, look for a later physical property or shorthand in the same logical property group.

Test at narrow and wide viewports. Arabic labels often occupy different space from English labels, which can reveal fixed widths, absolute controls and hidden overflow. Check open menus, selected tabs, sticky elements and overlays near both viewport edges. If the page scrolls sideways, use the RTL overflow guide to identify which box exceeds the document.

Use long Arabic fixtures and zoom. Confirm that text does not collide with an action that moved to inline end, borders remain attached to their intended edges, focus outlines are visible, and no clipping was introduced by a radius or overflow fix. Ritla checks R041, R042 and R043 name visible text overlap, clipped text with hidden overflow and fixed-width containers exceeded by Arabic text, respectively.

Test keyboard navigation after any flex, grid or order change. Test motion with reduced-motion preferences if the component animates. Check nested LTR content inside the Arabic route, especially maps, editors and imported widgets. A logical property follows the nearest direction context, so a nested boundary can move an edge by design. Confirm it is the edge you meant.

Ship the conversion without losing its meaning#

An LTR-to-RTL CSS conversion is finished when the code describes relationships rather than a screenshot. Flow-relative spacing uses logical properties. Flex and grid receive direction from markup. Physical coordinates stay physical. Transforms, shadows and directional icons have narrow, documented exceptions. The English layout still passes the baseline that existed before the work began.

A free Ritla scan can surface direction-sensitive physical properties without an RTL override R030, physical floats without an override R031, directional shadow offsets without a counterpart R032 and translateX movement that takes an element off-screen in RTL R033. Use those findings to keep the migration inventory honest, then use the RTL CSS guide when a component needs deeper work on a particular layout mechanism.

Convert one relationship at a time, test both directions, and delete the obsolete mirror once the browser can do the mapping. It is less dramatic than flipping a whole stylesheet. That is one of its better qualities.

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.