RTL CSS: the complete developer guide

Write CSS that works in both directions using logical properties, correct flex and grid behavior, scoped overrides, and repeatable RTL tests.

Guide13 min read

RTL CSS usually breaks where a component silently assumes that left means start and right means end. The assumptions hide in padding, absolute positioning, rounded corners, shadows, transforms, icon names and scripts that expect horizontal scroll offsets to be positive.

Changing the document to dir="rtl" already reverses the inline flow used by text, normal flex rows, grid auto-placement and table rows. It does not rewrite left, margin-left, translateX(), a shadow's x-offset or an SVG path. The job is to express logical relationships in CSS and keep physical values only where the design genuinely means a physical side.

Establish direction in HTML first#

CSS should respond to direction, not invent it. An Arabic document starts with:

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

The dir attribute gives descendants their base direction and defines the inline axis. lang="ar" does not set direction. R001 reports an Arabic page whose root direction is missing or LTR.

Avoid using CSS as the only source of document direction:

css
/* This changes rendering but omits the HTML direction. */
html[lang="ar"] {
  direction: rtl;
}

In Chromium 151, measured on 2026-09-17, CSS direction: rtl without a dir attribute displayed RTL, but the element did not match :dir(rtl). Firefox and Safari were not measured for the browser-specific observations in this guide, so run the same cases in the supported matrix.

Set the attribute in the server response when the locale is known. A client-only direction change can produce an LTR first paint, followed by a page-wide reflow. It can also leave error fallbacks or UI rendered before application startup in the wrong direction.

Start with logical properties#

CSS logical properties describe relationships to the writing mode instead of fixed viewport sides. In horizontal Arabic, inline start is right and inline end is left. Block start remains the top.

Use this mapping when the design means start or end:

Physical CSSLogical CSSUse when the intent is
margin-leftmargin-inline-startSpace before an item in inline flow
margin-rightmargin-inline-endSpace after an item in inline flow
padding-leftpadding-inline-startInner space at the starting edge
padding-rightpadding-inline-endInner space at the ending edge
leftinset-inline-startPosition from logical start
rightinset-inline-endPosition from logical end
border-leftborder-inline-startDivider at logical start
border-rightborder-inline-endDivider at logical end
widthinline-sizeSize along the inline axis
heightblock-sizeSize along the block axis
min-widthmin-inline-sizeMinimum inline measure
max-widthmax-inline-sizeMaximum inline measure

Do not replace physical properties mechanically. A map pin exactly 24 pixels from the physical left of an image should keep left. A timeline whose chronology always runs left to right may keep physical placement. Convert a property only when its meaning is logical.

Problem

css
.sidebar {
  border-right: 1px solid var(--rule);
  padding-right: 1.5rem;
}

In the English layout the sidebar sits at the start and its divider faces the content. Under RTL the grid puts the sidebar at the right, and the divider stays on its right-hand side, against the edge of the window instead of the content.

Direction-aware

css
.sidebar {
  border-inline-end: 1px solid var(--rule);
  padding-inline-end: 1.5rem;
}

R030 reports direction-sensitive physical properties without an RTL override. The long-term fix is usually a logical property in the shared component, not a growing list of Arabic-only corrections.

Use start and end for text alignment#

text-align: left and text-align: right name physical sides. start and end follow the element's direction:

css
.card-title,
.field-label,
.validation-message {
  text-align: start;
}

.card-actions {
  text-align: end;
}

This is different from setting base direction. Alignment chooses where the line sits inside its box. Direction controls bidi resolution and the inline axis. Right-aligning Arabic text does not turn an LTR container into an RTL one.

R022 reports explicit left alignment on majority-Arabic text. Treat the report as a prompt to identify intent. A left-aligned code sample may be correct; an Arabic validation message inherited from an English form is not.

text-indent is another directional property that is easy to miss. A positive indent applies from the line's start according to direction, but CSS built around negative or physical-looking indentation often assumes LTR geometry. Test list labels, hanging punctuation and decorative first lines under both directions. R032 reports direction-sensitive text indentation without an RTL counterpart, along with directional shadow offsets.

Let flexbox follow the container direction#

A normal flex row uses the inline direction of its container. In Chromium 151, the first item of a flex row under RTL appeared at the right edge. You do not need row-reverse to obtain that result.

Problem

css
.toolbar {
  display: flex;
}

[dir="rtl"] .toolbar {
  flex-direction: row-reverse;
}

The override reverses the RTL row again. In the measured browser, items then appeared left to right in DOM order, and Tab moved left to right. A screenshot can look intentional while visual progression, direction and keyboard order disagree.

Better

css
.toolbar {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

Use row-reverse only when the component's design truly needs the reverse of the current main axis. The flex-direction documentation warns that reversed visual order can disconnect layout from DOM and accessibility order. R081 reports row-reverse used to fake RTL when tab order fights visual order.

Prefer gap over directional margins between children. It avoids first-child and last-child exceptions and follows the flex or grid structure without knowing which side is start.

Do not use the order property to localize reading sequence. Keep DOM order meaningful, then let direction place inline items. If a component needs a different semantic order in Arabic, change the data or markup deliberately and retest screen-reader and keyboard flow.

Grid follows direction, but authored geometry still matters#

Grid auto-placement starts from the container's inline start. In the measured browser, the first auto-placed grid item appeared at the right edge in RTL. That makes a simple responsive card grid work without an RTL-specific template:

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

Explicit placement deserves review. Line numbers, named lines, absolutely positioned children and diagrams may carry assumptions about a physical left-to-right picture. Inspect them in both directions rather than assuming the browser should mirror the composition.

Named areas can express roles more clearly than selectors such as .left-panel and .right-panel, but the authored grid is still a design decision. Name regions by purpose, such as navigation, content and actions. Then decide whether the region belongs at logical start, logical end or a fixed physical side.

Tables also participate in direction. In Chromium 151, the first cell of an RTL table row sat at the right edge. Keep cell content semantics separate: an Arabic description can be RTL while an email, ID or code cell remains LTR.

Position overlays with logical insets#

Absolute, fixed and sticky elements often reveal physical assumptions first. Search icons overlap text, badges attach to the wrong corner and drawers enter from the wrong side.

Physical positioning

css
.notification-badge {
  position: absolute;
  top: -0.25rem;
  right: -0.25rem;
}

Logical positioning

css
.notification-badge {
  position: absolute;
  inset-block-start: -0.25rem;
  inset-inline-end: -0.25rem;
}

The logical version is correct only if the badge belongs at the component's ending corner. A status light that represents a physical sensor location should remain physical.

Use logical corner radii when a component has an asymmetric shape tied to start or end:

css
.tab {
  border-start-start-radius: 0.75rem;
  border-end-start-radius: 0.75rem;
}

Do not duplicate all four radius values in an RTL selector unless the supported browser matrix requires a fallback. Keep any fallback next to the logical declaration and test the cascade so both do not compete.

Sticky elements need overflow testing, not only edge inspection. A sticky first column, horizontal table and RTL scroll container can interact with direction, stacking and clipping. Test from both horizontal extremes with real content.

Transforms do not become logical#

translateX(), rotation and scaling use authored geometry. A negative horizontal translation remains negative when the document becomes RTL.

A slide carousel built on a transformed track shows the problem cleanly:

css
.track {
  display: flex;
  transform: translateX(calc(var(--slide) * -100%));
}

.slide {
  flex: 0 0 100%;
}

Under RTL the flex row already starts at the right, so the second slide sits to the left of the first. Moving the track left by a slide's width carries the second slide further out of view instead of into it. Measured in Chromium 151, a three-slide track set to the second slide showed no slide at all in RTL. The fix is to make the step direction a variable:

css
.track {
  display: flex;
  --step: -100%;
  transform: translateX(calc(var(--slide) * var(--step)));
}

.track:dir(rtl) {
  --step: 100%;
}

With that change the same track showed the second slide in both directions. The layout uses logical flow; the motion changes its physical sign, because CSS has no translate-inline() function. R033 reports a translateX that moves an element off-screen in RTL.

Test every animation phase, not only the resting state: the first, middle and last slide, and any transition between them. State classes and keyframes can replace the direction-specific transform later in the cascade.

If an icon uses transform for hover or animation, a separate RTL rule such as scaleX(-1) may overwrite it. Wrap the SVG, combine transform functions or choose a separate directional asset. Keep direction transforms on one layer and state transforms on another.

Floats are usually a migration smell#

Legacy layouts often use float: left or float: right for labels, images and actions. R031 reports physical floats on content elements in an RTL context without an override.

Modern components are usually easier to express with flexbox or grid. When a float is genuinely for text wrapping, use a logical value supported by the target browsers or provide a tested direction override:

css
.article-figure {
  float: inline-start;
  margin-inline-end: 1rem;
  margin-block-end: 0.5rem;
}

Do not retain a float solely because an RTL override already exists. A pair of physical rules can reproduce the layout, but it also leaves clearing, source order and responsive behavior tied to an old abstraction.

Shadows, gradients and backgrounds need a meaning check#

Horizontal shadow offsets do not mirror. Whether they should depends on what the shadow represents.

A drawer-edge shadow that indicates separation from adjacent content should move with the drawer. A page-wide light source should remain physically consistent. R032 reports direction-sensitive shadow x-offsets without an RTL counterpart, but the correct response is to inspect semantics, not reverse every shadow.

Use a custom property when the offset is logical:

css
.side-panel {
  --shadow-x: 0.5rem;
  box-shadow: var(--shadow-x) 0 1rem rgb(0 0 0 / 0.18);
}

:dir(rtl) .side-panel {
  --shadow-x: -0.5rem;
}

Background images, gradients and background-position also keep their authored geometry. Decorative texture may stay unchanged. An arrow baked into a background or a gradient used to signal overflow at inline end may need an RTL variant.

Avoid flipping the whole component with scaleX(-1). It reflects text, numbers, photos and shadows along with the intended shape. Apply direction to the smallest graphical layer that carries directional meaning.

Mirror icons by meaning, not by shape#

Back, forward, previous, next and breadcrumb arrows normally follow interface direction. Search, close, settings, download, play, pause, clocks, maps and brand marks usually keep their geometry. The same arrow can mean logical progression in one component and physical movement in another.

Use semantic icon names and an explicit mirror flag:

css
.icon--mirrors {
  transform: scaleX(1);
}

:dir(rtl) .icon--mirrors {
  transform: scaleX(-1);
}

R025 reports a directional icon that points against the way its control moves, such as a "next" chevron pointing back, and flags unmirrored icons named for a side for review. The filename does not prove what the icon means.

SVG paths do not mirror automatically under RTL. This is useful for logos, charts and physical diagrams. If a directional SVG also contains text, do not reflect the root SVG because the text will reflect too. Mirror only the arrow group or provide an RTL asset.

The close button illustrates the split between placement and shape. Its position may move to the component's logical end, while the x stays unchanged.

Keep Arabic typography out of Latin CSS treatments#

Shared typography utilities can damage Arabic even when layout direction is correct.

Positive letter-spacing pulls Arabic's connected script apart. Uppercase has no Arabic equivalent, and a Latin display utility that combines uppercase with tracking should not flow into Arabic. Italic or oblique styling can also be an unintended inherited choice.

Scope script-specific styles by language or use tokens chosen for each typographic system:

css
:lang(ar) {
  letter-spacing: normal;
  text-transform: none;
  font-style: normal;
}

R051 reports positive letter spacing on Arabic text. R054 reports italic or oblique styling, and R055 reports Latin display styling applied to Arabic. These declarations may come from a generic heading class several layers above the visible text.

Inspect the font that actually rendered after web fonts load. A declared Latin family can fall through for Arabic and change widths, baselines and line breaks. Do not compensate for fallback geometry with direction-specific negative margins.

Treat overflow as directional evidence#

On an RTL page, horizontal overflow often appears on the left, where an LTR debugging habit may not look first. In Chromium 151, content extending past the left edge added horizontal scrollable width; equivalent content past the right edge did not. LTR behaved in the opposite direction.

At each supported viewport:

  1. compare the document's scrollWidth and clientWidth;
  2. scroll to both horizontal extremes;
  3. inspect fixed and absolutely positioned elements near each edge;
  4. trigger long validation, menus, dialogs, toasts and tooltips;
  5. repeat after fonts and asynchronous content load.

R040 reports document-level horizontal overflow. R041, R042 and R043 cover overlapping text-bearing elements, clipped text and fixed-pixel containers whose Arabic text exceeds the space.

Do not set overflow-x: hidden on the root as a general RTL patch. It hides evidence while content can remain unreachable. Find the overflowing child, then decide whether its size, position, transform or text constraint is wrong.

Prefer content-driven sizes. Arabic labels often occupy different widths from their English sources, and fixed action widths turn a translation change into clipping.

RTL scrolling uses a different offset range#

Carousels, tab strips and wide tables can render correctly while their JavaScript controls move in the wrong direction. In an RTL scroll container, scrollLeft was 0 at inline start and negative toward the end in Chromium 151.

In that browser, scrollBy({ left: 300 }) did nothing at the RTL start and moved back toward the start from an intermediate position. Code that clamps scroll offsets to zero or assumes the end is a positive maximum will fail.

Represent movement as next, previous, start and end in application code. Isolate browser-coordinate conversion in one helper, then test which item becomes visible instead of asserting only a raw number.

Test mouse, keyboard, touch and control buttons from the start, middle and end. Confirm that button labels, arrow direction and actual movement agree. Overscroll details can differ between engines, so measure the supported browsers.

Choose selectors that survive nested direction#

Root selectors are common:

css
[dir="rtl"] .breadcrumb-icon {
  transform: scaleX(-1);
}

They work for a page with one direction, but they still match a breadcrumb nested inside dir="ltr". :dir() expresses the element's directional context:

css
.breadcrumb-icon:dir(rtl) {
  transform: scaleX(-1);
}

Use :dir() for reusable components that may be nested in opposite-direction regions. Keep the earlier browser caveat in mind: CSS-only direction did not make the element match :dir() in the measured Chromium version. Direction should come from HTML semantics.

Do not attach direction to a locale class deep in the application when the root attribute already exposes it. Multiple sources of truth drift during client-side switching and tests.

Audit pseudo-elements, markers and generated content#

Directional details often live outside the main declarations. A breadcrumb separator may come from ::before, a step arrow from ::after, and list indentation from physical padding. These rules are easy to miss in a search limited to component markup.

Problem

css
.breadcrumb-item + .breadcrumb-item::before {
  content: ">";
  margin: 0 0.5rem 0 0.25rem;
}

.feature-list {
  padding-left: 1.5rem;
}

The separator has directional meaning, while the list padding is fixed to the left.

Direction-aware

css
.breadcrumb-item + .breadcrumb-item::before {
  content: ">";
  margin-inline: 0.25rem 0.5rem;
}

.breadcrumb-item:dir(rtl) + .breadcrumb-item::before {
  content: "<";
}

.feature-list {
  padding-inline-start: 1.5rem;
}

This example assumes the separator means progression through the breadcrumb. If it represents a mathematical greater-than sign, it should not be localized as an arrow.

Keep meaningful labels in HTML or localization resources, not CSS content. Generated content is a poor home for copy that must be translated, reviewed and exposed consistently to assistive technology. CSS is suitable for a decorative separator when the actual link names and navigation semantics are present in markup.

Inspect ::marker, counters, quotes, badges and icons supplied by pseudo-elements. Test wrapped list items at narrow widths, because a marker can appear on the expected side while continuation lines still use physical padding. Also search for literal arrow characters in stylesheets and for instructions such as “use the left arrow.” R082 reports UI text that refers to physical arrow directions.

Give components a direction contract#

Logical properties remove many variants, but transforms, directional artwork and some shadows still need explicit values. Centralize those decisions at the component boundary instead of scattering root selectors through unrelated files.

css
.drawer {
  --drawer-closed-x: -100%;
  --drawer-shadow-x: 0.5rem;
  --back-icon-scale: 1;

  inset-inline-start: 0;
  transform: translateX(var(--drawer-closed-x));
  box-shadow: var(--drawer-shadow-x) 0 1rem rgb(0 0 0 / 0.18);
}

.drawer:dir(rtl) {
  --drawer-closed-x: 100%;
  --drawer-shadow-x: -0.5rem;
  --back-icon-scale: -1;
}

.drawer__back-icon {
  transform: scaleX(var(--back-icon-scale));
}

The custom properties document which values vary by direction. They also keep state selectors from reimplementing the mapping. A component test can inspect these values in both contexts and verify the resulting geometry.

Do not turn every horizontal number into a direction token. Padding expressed with logical properties needs no variant. A symmetric icon needs no scale token. Keep the contract limited to exceptions that cannot be expressed logically.

For component libraries, include LTR and RTL examples in the same test page, each under its own dir container. That catches selectors that assume the root direction and exposes CSS variables or assets that leak between instances. Add long Arabic labels and an LTR value so the example tests content boundaries as well as box placement.

Migrate legacy CSS by component#

A global search-and-replace from left to start can change physical designs accidentally. Migrate one component family at a time:

  1. identify every physical horizontal declaration;
  2. label its intent as logical, physical or unresolved;
  3. convert logical spacing, insets, borders and radii;
  4. remove duplicate RTL overrides made unnecessary by logical CSS;
  5. isolate transforms, icons, shadows and backgrounds that still need variants;
  6. test LTR and RTL at hostile widths and dynamic states;
  7. record any intentional physical value in the component contract.

Generated RTL stylesheets can help a legacy product, but they are not a semantic review. A build tool cannot know whether left means inline start, west on a map or the physical side of a medical image. Automatic flipping can also double-reverse a rule that is already logical or has a manual override.

Prefer a shared logical base for new work. Keep direction-specific CSS small, named for the semantic exception and located with the component it changes.

Test CSS as a behavior matrix#

For each representative component, test both directions with short and long content, minimum and wide viewports, loaded and failed data, and keyboard operation. Inspect computed styles rather than relying only on screenshots.

AreaEvidence to capture
DirectionRoot dir, component computed direction, nested overrides
SpacingComputed logical margins and padding on both sides
LayoutDOM order, flex or grid settings, visual order, focus path
PositioningInsets, containing block, bounding boxes at both edges
MotionClosed, entering, open and exiting transform values
GraphicsIcon meaning, mirrored layer, unchanged text and logos
TypographyRendered font, size, line height, tracking and style
OverflowDocument width, clipped boxes, both scroll extremes
ScrollersVisible item after next, previous, start and end actions

Ritla reports physical CSS, transforms, layout and overflow failures through checks such as R030, R033, R040 and R081. Run a free scan of a migrated route, then reproduce each finding in the component state that exposes it.

For the design decisions behind icons and motion, see what should be mirrored in RTL. When a migrated page still scrolls sideways, how to fix horizontal overflow in RTL finds the box responsible.

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.