RTL flexbox: common problems and fixes
Learn how flexbox follows RTL direction, why row-reverse and order cause failures, and how to fix margins, wrapping, focus order and overflow.
Guide15 min read
Flexbox already knows where an RTL row starts. Give the container the correct direction, leave flex-direction at row, and the first item sits on the right. Adding row-reverse to “make it RTL” reverses a row that the browser has already reversed for you.
That extra rule can look convincing in one screenshot. It can also put the first item on the left, separate visual order from source order, move auto margins to the wrong side and turn keyboard testing into a small guided tour of the container.
The useful model is not “RTL means reverse Flexbox.” It is this:
direstablishes the writing direction.flex-directionchooses the main axis and whether that axis is reversed.- DOM order remains the logical reading and navigation order.
- Logical margins follow the writing direction; physical margins do not.
- Alignment values may follow the writing direction or the flex flow, depending on which value you choose.
Once those jobs stay separate, most RTL Flexbox fixes become deletion rather than invention. This is encouraging. CSS rarely offers refunds.
How direction changes a flex row#
The initial flex-direction is row. According to the CSS Flexible Box Layout specification, a row's main axis has the same orientation as the current writing mode's inline axis. In a horizontal English container, inline start is left. In a horizontal Arabic container, inline start is right.
For the horizontal writing mode used by ordinary English and Arabic interfaces:
| Container direction | flex-direction | Main start | First DOM item appears at |
|---|---|---|---|
| LTR | row | Left | Left |
| LTR | row-reverse | Right | Right |
| RTL | row | Right | Right |
| RTL | row-reverse | Left | Left |
| LTR or RTL | column | Top | Top |
| LTR or RTL | column-reverse | Bottom | Bottom |
row-reverse does not mean RTL. It swaps main start and main end for the row that already exists. On an RTL container, that makes the row run left to right. The MDN flex-direction reference documents the same mapping.
Columns need one extra thought. Changing LTR to RTL does not turn a top-to-bottom column into a bottom-to-top column because its main axis is the block axis. Its cross axis is the inline axis, however. In an RTL column, cross-start is on the right, so align-items: flex-start places items at the right edge. The column did respond to direction. It simply responded sideways.
Here is the compact axis map:
| Flex direction | Axis controlled by justify-content | Axis controlled by align-items | What RTL changes in horizontal writing |
|---|---|---|---|
row | Horizontal main axis | Vertical cross axis | Main start moves from left to right |
column | Vertical main axis | Horizontal cross axis | Cross start moves from left to right |
If an alignment rule seems to ignore RTL, first identify the main and cross axes. “Horizontal” and “vertical” are results, not the names Flexbox reasons with. It is fond of abstraction, and it did bring diagrams.
Let dir establish RTL, not row-reverse#
Consider a transit service advisory with three actions in meaningful source order. The first action should appear at reading start: left in English and right in Arabic.
Problem
<html lang="ar">
<body>
<div class="advisory-actions">
<a href="/affected-stops">عرض المحطات المتأثرة</a>
<button type="button">متابعة التحديثات</button>
<a href="/service-notice">تنزيل الإشعار</a>
</div>
</body>
</html>.advisory-actions {
display: flex;
flex-direction: row-reverse;
gap: 0.75rem;
}This code leaves the document direction at its LTR default and uses row-reverse to put the first item on the right. It changes the visual flow of this one flex container. It does not give the document an RTL base direction, make logical properties resolve as RTL, or give Arabic text the right surrounding bidi context.
The layout is imitating one visible consequence of RTL while the document remains LTR. It is a stage set. The building behind it is not finished.
Better
<html lang="ar" dir="rtl">
<body>
<div class="advisory-actions">
<a href="/affected-stops">عرض المحطات المتأثرة</a>
<button type="button">متابعة التحديثات</button>
<a href="/service-notice">تنزيل الإشعار</a>
</div>
</body>
</html>.advisory-actions {
display: flex;
flex-direction: row;
gap: 0.75rem;
}The Arabic page now has semantic RTL direction, and the first action sits at the row's right-hand main start. On the English route, use lang="en" dir="ltr"; the same CSS places the first action at the left. The LTR layout has not changed.
You can omit flex-direction: row because it is the initial value. Keeping it in a component rule can be useful when the row contract deserves to be obvious. What matters is that direction comes from the content and the row is not reversed merely to simulate it.
If the page direction is missing, contradicted or declared only in CSS, R001 names that document-level failure. If row-reverse is being used to fake RTL instead of dir, R081 names the resulting problem, including the conflict between tab order and visual order. The two rules may appear together, but they are not the same bug.
For more on setting direction at the document and component boundaries, use the HTML dir="rtl" guide. This page will stay with Flexbox, which already has enough axes for one afternoon.
DOM order is still the real order#
RTL changes where a logical sequence begins. It does not mean the semantic sequence in the markup should be written backwards.
Suppose the DOM contains actions A, B and C in the order a user should encounter them. In an LTR row, their physical positions run A to B to C from left to right. In an RTL row, they run A to B to C from right to left. The visual direction changed; the logical sequence did not.
That is why a correctly directed row does not need reversed markup. It is also why row-reverse and the order property deserve caution. The Flexbox specification says these reordering features affect visual rendering while speech and sequential navigation continue to use source order. It explicitly warns against using them as a substitute for correct source order.
An order fix that creates a focus jump#
The transit team wants “Follow updates” to be the first action. Moving it with CSS gives the desired pixels:
Problem
<div class="advisory-actions">
<a href="/affected-stops">عرض المحطات المتأثرة</a>
<button class="advisory-actions__primary" type="button">متابعة التحديثات</button>
<a href="/service-notice">تنزيل الإشعار</a>
</div>.advisory-actions {
display: flex;
gap: 0.75rem;
}
.advisory-actions__primary {
order: -1;
}Items with a lower order value are painted earlier in the flex order. On the RTL row, the primary button therefore appears at the right-hand start, followed by the two links. The sequential focus order still follows the DOM: the first link, then the visually first button, then the last link. Focus moves from the middle to the right and then across to the left.
If the action is genuinely first in meaning and navigation, put it first in the source:
Better
<div class="advisory-actions">
<button type="button">متابعة التحديثات</button>
<a href="/affected-stops">عرض المحطات المتأثرة</a>
<a href="/service-notice">تنزيل الإشعار</a>
</div>.advisory-actions {
display: flex;
gap: 0.75rem;
}This preserves the same visual result in LTR and RTL while making source, reading and focus order agree. It also survives a future non-flex rendering, reader mode or stylesheet failure without turning the primary action into a scavenger hunt.
order is not forbidden. It can adjust purely visual, non-focusable material when source order remains meaningful without the visual arrangement. It becomes risky when the reordered items form a process, a reading sequence or a set of interactive controls. The MDN guide to ordering flex items explains the source-order disconnect and recommends keyboard testing.
In Chromium 151, checked on 2026-09-24, an LTR row-reverse put the first DOM item at the right and Tab began there. Inside an RTL container, row-reverse laid the items left to right in DOM order and Tab moved left to right. Firefox and Safari were not measured for those fixtures. The standards-level lesson is broader: CSS reordering does not repair a source sequence that was wrong to begin with.
Replace physical auto margins with logical ones#
Auto margins are useful in Flexbox because they absorb available space on their side. A common action bar uses one to push a trailing control away from the leading content.
For an LTR row, this physical rule appears to work:
Problem
<header class="line-summary">
<div class="line-summary__copy">
<strong>المسار الساحلي</strong>
<span>الخدمة منتظمة</span>
</div>
<button class="line-summary__alerts" type="button">تنبيهات المسار</button>
</header>.line-summary {
display: flex;
align-items: center;
gap: 1rem;
}
.line-summary__alerts {
margin-left: auto;
}In LTR, the button's left margin sits between the copy and the button. It expands and pushes the button to the right. In RTL, margin-left remains on the physical left, which is after the button at inline end. It no longer expresses “put flexible space before this trailing action.”
Use the logical start margin on the item you want to push toward inline end:
Better
<header class="line-summary">
<div class="line-summary__copy">
<strong>المسار الساحلي</strong>
<span>الخدمة منتظمة</span>
</div>
<button class="line-summary__alerts" type="button">تنبيهات المسار</button>
</header>.line-summary {
display: flex;
align-items: center;
gap: 1rem;
}
.line-summary__alerts {
margin-inline-start: auto;
}In LTR, margin-inline-start maps to the left margin, so the original layout is unchanged. In RTL, it maps to the right margin, which lies between the leading copy and the trailing button. The auto margin absorbs the free space there and pushes the button to inline end on the left.
This rule assumes the DOM order is leading content followed by the trailing action and the flex direction is row. If the component intentionally uses row-reverse, main start and inline start no longer point the same way. That is another reason not to mix a reverse flow into an ordinary bilingual action bar. When reversal is genuinely part of the design, draw the main axis before choosing which logical margin should absorb space.
Ritla check R030 covers direction-sensitive physical properties without an RTL override. It does not decide whether the intended meaning was start, end or a fixed physical side. That decision still belongs to the component.
Use gap for spacing between siblings#
Equal space between flex items is not a start-side or end-side relationship. It is a gap between adjacent items. Say so directly:
.advisory-actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem 1rem;
}gap inserts gutters between flex items and, when wrapping occurs, between flex lines. It does not need an RTL override and does not create an unwanted outer margin at either edge.
Compare that with a physical sibling rule:
.advisory-actions > * + * {
margin-left: 1rem;
}The selector follows DOM adjacency while the margin stays physically left. In an RTL row, that may put space on the outer side of an item rather than between it and the previous visual item. You could convert it to margin-inline-start, but gap better describes equal inter-item spacing and behaves more cleanly when items wrap.
Margins still have jobs. An auto margin can split one group from another, and a logical margin can give one item deliberate extra separation. Use gap for the rhythm shared by every sibling. Use a margin for a relationship that belongs to one item. It is a small distinction with a surprising ability to prevent large selectors.
Know whether alignment follows writing direction or flex flow#
justify-content aligns the flex items along the main axis. In a normal row, start and flex-start point to the same edge. Reverse the row and they stop agreeing.
The MDN flex alignment guide explains the distinction:
flex-startandflex-endfollow the flex container's main-start and main-end. Reversingflex-directionswaps them.startandendare flow-relative alignment values. Changingrowtorow-reversedoes not swap them.
For a horizontal container:
| Direction and flow | justify-content: flex-start | justify-content: start |
|---|---|---|
LTR row | Left | Left |
LTR row-reverse | Right | Left |
RTL row | Right | Right |
RTL row-reverse | Left | Right |
Use flex-start when items should gather at the start of the chosen flex flow. Use start when they should gather at the writing-mode start edge even if the flow has been reversed. In the usual bilingual row with no reversal, either reaches the right edge in RTL. There is no prize for changing a working flex-start to start unless the distinction matches an actual requirement.
Distributed values such as space-between do not decide which item belongs at which edge. They distribute leftover space after flex order and direction have established the sequence. With three items, the first flex item occupies main start and the last occupies main end. If the wrong item is at an edge, inspect direction, source order, flex-direction and order before blaming space-between.
For a column, justify-content works vertically because the main axis is vertical. RTL shows up on the horizontal cross axis instead, through values such as align-items: flex-start. This is why copying a row alignment fix into a column component can produce a technically valid declaration aimed at the wrong dimension. The browser is not being difficult. It is being literal with considerable enthusiasm.
Wrapping has a direction too#
With flex-wrap: wrap, items fill a flex line from main start to main end. In an RTL row, each line begins at the right and proceeds left. New lines stack from cross-start to cross-end, which is top to bottom in the ordinary horizontal writing mode.
.service-filters {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}No RTL override is needed. The first filter remains first in the DOM and appears at the right-hand start of the first line. When the viewport narrows, later filters continue onto the next line.
wrap-reverse is not the wrapping equivalent of RTL. It swaps cross-start and cross-end, so the flex lines stack in the opposite cross-axis direction. In a horizontal row, that means reversing whether lines accumulate downward or upward. It does not reverse the item sequence inside each line.
Wrapping can still expose a product problem. A sequence that reads clearly on one line may become ambiguous when split across two, especially if items are numbered, connected by arrows or meant to describe stages. Flexbox preserves source order, but it does not add context to the second line. At narrow widths, verify where the line breaks and whether the reading path remains understandable from the right-hand start.
Also distinguish two similar properties:
align-itemsaligns items within each flex line.align-contentdistributes multiple flex lines when the container has extra cross-axis space.
If a wrapped group has no extra cross-axis space, changing align-content may appear to do nothing. That is not an RTL bug. It is merely a property declining a job vacancy that does not exist.
Let text-bearing flex items shrink#
An Arabic translation often reaches the Flexbox bug that a short English fixture politely avoided. Flex items have an automatic minimum size based on their content in the main axis. A text-bearing item can therefore refuse to become narrower than its minimum-content size, forcing a sibling out or expanding the container.
Consider an advisory with an icon, a message and a dismiss button:
<section class="service-alert">
<span class="service-alert__icon" aria-hidden="true">●</span>
<p class="service-alert__message">سيستمر تغيير المسار حتى اكتمال أعمال الصيانة المسائية.</p>
<button type="button">إغلاق</button>
</section>.service-alert {
display: flex;
align-items: flex-start;
gap: 0.75rem;
}
.service-alert__message {
flex: 1;
}If the message item cannot shrink as the design expects, set its logical minimum inline size explicitly:
.service-alert__message {
flex: 1;
min-inline-size: 0;
}The Flexbox specification's automatic minimum-size rules explain why flex items do not necessarily shrink below their content-based minimum. min-inline-size: 0 says this item may shrink along the inline axis, after which normal text wrapping can use the available width.
This is not permission to hide text. If an unbroken URL or technical token still overflows its own box, give that content an appropriate wrapping or isolation rule. Do not apply overflow: hidden to the whole message and congratulate the card on becoming shorter. The horizontal overflow guide covers the wider page-level diagnosis.
On a narrow RTL page, overhang on the left can create document-level horizontal scrolling. Ritla check R040 names document-level horizontal overflow. It does not say Flexbox caused it, so inspect the flex item's minimum size, fixed children, transforms and physical offsets before choosing a fix.
Nested flex containers follow their own direction boundary#
flex-direction is not inherited. Direction is. A nested element that becomes a flex container will use its own flex-direction value, usually the initial row, and the direction it receives from its nearest dir boundary.
This is useful. An RTL service card can contain an explicitly LTR technical strip:
<article class="service-card" dir="rtl" lang="ar">
<h2>تفاصيل الخدمة</h2>
<div class="service-card__system" dir="ltr" lang="en">
<span>Control centre</span>
<span>Platform status</span>
</div>
</article>.service-card__system {
display: flex;
gap: 0.75rem;
}The outer card is RTL. The nested strip establishes LTR direction, so its default flex row starts at the left. You do not need row-reverse, per-child order values or an RTL selector to undo the parent. The local direction boundary does the work.
Set a local direction only when the content is genuinely LTR. Do not force an entire container to LTR because one code, phone number or icon inside it needs special handling. That changes the flex row as well as the text context. Isolate the exceptional value or control instead. For mixed inline content, the bidirectional text guide is the relevant next step.
Direction does not mirror every flex item#
Changing a row to RTL changes the main-axis direction. It does not mirror the pixels inside each item.
A chevron that means “open details” may need to point toward the revealed content. A media playback icon usually keeps its familiar shape. A map thumbnail does not become geographically ambitious. Flexbox moves the item as a box; it does not decide whether the asset itself should flip.
Keep that decision separate from item order. If a directional icon needs mirroring, style the icon under a semantic direction boundary. Do not reverse the whole row to make one arrow point the other way. The guide to what should be mirrored in RTL covers that asset-level decision, and R025 names a directional icon pointing against the sequence its control advances.
Debug the axes before adding an override#
When an RTL flex component looks wrong, inspect the container in browser DevTools and answer these questions in order:
- What element establishes
dir="rtl", and does the flex container inherit it? - What is the container's computed
direction? - Is
flex-directionrow,row-reverse,columnorcolumn-reverse? - Where are main start and cross start for that combination?
- Do any children have nonzero
ordervalues? - Does an auto margin use a logical or physical side?
- Is spacing coming from
gap, sibling margins or both? - Is a text-bearing child refusing to shrink because of its automatic minimum size?
If your browser DevTools provides a Flexbox overlay, use it to identify the main axis, gaps and item sizes. Still inspect computed styles. A row that looks reversed may come from direction, row-reverse, source order, order, or an auto margin that consumed all the free space. The screenshot reports the outcome. The computed styles reveal the plot.
Remove suspected declarations one at a time. Start with row-reverse, child order values and physical auto margins. If the component falls into the intended RTL arrangement after a rule is disabled, replace the rule's underlying assumption rather than covering it with a more specific selector.
Test RTL Flexbox as a sequence, not a picture#
A visual check catches the large errors. A keyboard and narrow viewport catch the expensive ones.
Use this test matrix for each reusable flex component:
| Test | What to verify |
|---|---|
| LTR route | First logical item begins at the expected LTR edge |
| RTL route | The same first logical item begins at the expected RTL edge |
| Keyboard only | Focus follows a meaningful path without jumping across reordered items |
| Narrow viewport | Wrapping keeps the sequence understandable and creates no horizontal scroll |
| Long Arabic text | Text wraps, flex items shrink as intended and controls remain visible |
| Nested direction | A local LTR or RTL container starts its own row at its own inline start |
| Zoomed text | Buttons and labels wrap without overlap or clipped actions |
| Dynamic content | Added, removed and conditionally hidden items preserve source order |
For an interactive row, perform the keyboard test from a fresh page load. Press Tab through every control and watch both the focus indicator and the visual path. Do not infer focus order from DOM inspection alone when CSS reordering is present. The whole point is to observe the mismatch a user would receive.
At each breakpoint, check the bounding edge of the first DOM item. In a normal horizontal row it should be on the left under LTR and on the right under RTL. If it is not, inspect flex-direction, order and the container's computed direction before changing markup.
Test wrapping with real Arabic labels rather than duplicated short placeholders. Then add one unusually long translation and one fixed-size child. Flex sizing problems tend to wait until the component contains both. Very considerate of them.
Finally, check the page itself for horizontal scrolling. A component can look contained while a child extends the document's scrollable width beyond the viewport. Test at the narrowest supported width and after increasing text size, not only at a comfortable desktop breakpoint.
Keep the row logical and the exceptions local#
The stable RTL Flexbox implementation is generally the quiet one: meaningful DOM order, correct dir, an ordinary row, gap between peers and logical margins where one item must move away from another. Reverse values and order remain available for designs that truly require visual reordering, but they should not be the price of entering Arabic.
A free Ritla scan can surface a flex row using row-reverse to fake RTL R081, direction-sensitive physical properties without an RTL override R030, and document-level horizontal overflow R040. Keyboard order, intentional reverse flows and whether a margin represents start or end still need human judgment.
If the row is now correct but the rest of the component still contains physical side assumptions, continue with the RTL CSS guide. The flex items should follow the language. They do not need choreography.
Checks in this guide
- R001document direction missing, contradicted, or declared only in CSS
- R081flex row-reverse faking RTL instead of dir (tab order fights visual order)
- R030Direction-sensitive physical properties without an RTL override
- R040Document-level horizontal overflow
- R025Directional icon pointing against the sequence its control advances