RTL CSS grid: how grid layouts mirror
Learn how CSS Grid lines, areas, auto-placement and alignment behave in RTL, and fix physical placement, dense ordering and overflow bugs.
Guide16 min read
Switch a grid container to RTL and column line 1 moves to the right. The first auto-placed item moves there too. The first track in grid-template-columns becomes the rightmost track, and the first cell named in a template row follows it.
Then the absolutely positioned badge inside the grid stays nailed to left: 0, because it was given a physical instruction and intends to carry it out.
That is the central distinction in RTL CSS Grid: Grid placement is flow-relative, but not every property used inside a grid is. Lines, tracks, named areas, auto-placement and logical alignment respond to direction. Physical offsets, margins, transforms and left-or-right assumptions do not.
The fix is rarely to write a second grid backwards. It is to let direction mirror the parts that should follow reading order and identify the smaller set of things that should remain physically fixed. Grid can do the first part without assistance. It has a specification and everything.
What CSS Grid mirrors in RTL#
In the horizontal writing mode used by ordinary English and Arabic interfaces, direction changes the inline axis. Rows still run from top to bottom, while columns run from inline start to inline end.
| Grid concept | LTR result | RTL result |
|---|---|---|
Column line 1 | Left edge of explicit grid | Right edge of explicit grid |
Column line -1 | Right edge of explicit grid | Left edge of explicit grid |
| First declared column track | Leftmost track | Rightmost track |
| First auto-placed item | First available cell from the left | First available cell from the right |
justify-self: start | Item aligns left within its area | Item aligns right within its area |
justify-content: start | Track grid aligns left | Track grid aligns right |
Row line 1 | Top edge | Top edge |
align-self: start | Item aligns to block start, normally top | Same |
The CSS Grid specification defines grid placement in terms of inline start, inline end, block start and block end. MDN's guide to Grid and writing modes shows the practical consequence: line 1 sits on the right in an RTL grid and line -1 sits on the left.
The table assumes writing-mode: horizontal-tb. In a vertical writing mode, the inline axis is vertical and the same logical rules produce different physical sides. “Line 1 is on the right in RTL” is a useful result for Arabic web interfaces, not the definition of line 1.
Direction also belongs to the grid container, not necessarily the whole document. A nested grid with dir="ltr" will number and auto-place its columns from the left even inside an RTL page. That is often correct for a genuinely LTR component. It is not a suitable way to keep one icon from moving.
The page-level fixture in this guide was checked in Chromium 151 on 2026-09-24. Its first auto-placed grid item sat at the right edge under dir="rtl". Firefox and Safari were not measured for that fixture. The behavior itself also follows the Grid specification's flow-relative placement rules.
Named areas already follow inline direction#
Consider a building-permit review workspace with a review queue at reading start, a plan in the center and inspector notes at reading end.
<main class="permit-workspace">
<nav class="permit-workspace__queue" aria-label="طلبات المراجعة">
طلبات المراجعة
</nav>
<section class="permit-workspace__plan">مخطط الموقع</section>
<aside class="permit-workspace__notes">ملاحظات المفتش</aside>
</main>The LTR stylesheet names the areas in start-to-end order:
.permit-workspace {
display: grid;
grid-template-areas: "queue plan notes";
grid-template-columns:
minmax(14rem, 18rem)
minmax(0, 1fr)
minmax(16rem, 20rem);
gap: 1rem;
}
.permit-workspace__queue { grid-area: queue; }
.permit-workspace__plan { grid-area: plan; }
.permit-workspace__notes { grid-area: notes; }On an LTR page, the first area token and first track appear on the left. On an RTL page, the grid's first column begins at inline start on the right. The same template therefore places queue on the right and notes on the left. That is the desired mirror when those regions mean reading start and reading end.
A tempting RTL override reverses the template text manually:
Problem
[dir="rtl"] .permit-workspace {
grid-template-areas: "notes plan queue";
}The container has already changed its inline direction. Reversing the tokens as well puts notes back at the right and queue at the left. The override cancels the semantic mirror while looking exactly like code written to create one. A fine little trap.
Better
.permit-workspace {
display: grid;
grid-template-areas: "queue plan notes";
grid-template-columns:
minmax(14rem, 18rem)
minmax(0, 1fr)
minmax(16rem, 20rem);
gap: 1rem;
}Use the same template in both directions when the first area is logically at start and the last is logically at end. The English layout stays unchanged, and the Arabic layout mirrors through direction rather than a second template.
Area names are identifiers, not instructions. A name such as left-panel does not remain on the left, and Grid does not reinterpret it as right-panel in RTL. Prefer names that describe the region's job, such as queue, content, notes, summary or tools. If a panel truly belongs to a physical side in every locale, make that exception explicit instead of relying on an optimistic class name.
Track lists are ordered from inline start#
The values in grid-template-columns follow the grid's column axis. In this two-track declaration, the fixed track is the first track:
.review-layout {
display: grid;
grid-template-columns: 18rem minmax(0, 1fr);
gap: 1.25rem;
}Under LTR, the 18rem track appears on the left. Under RTL, it appears on the right. The declaration did not change, but “first” moved with inline start.
This is useful for start-side navigation, summaries and controls. It is surprising when the first track was intended to mean “physical left.” Track sizes have no built-in awareness of the component's product role. They follow track order.
The same applies inside repeat() and minmax(). Direction changes the placement of the resulting tracks, not their sizes. repeat(3, 1fr) remains three equal columns. The first equal column is simply on the right in RTL, which is less dramatic because all three have made a coordinated effort to look identical.
If your design uses unequal tracks, test the mirrored result early. A start-side rail that should mirror usually works without an override. A physically fixed data visualization may need a deliberate local coordinate direction, covered later in this guide.
Grid line numbers are logical, not physical#
Numeric placement mirrors for the same reason. With three explicit columns:
.permit-workspace {
display: grid;
grid-template-columns: 16rem minmax(0, 1fr) 18rem;
}
.permit-workspace__queue {
grid-column: 1 / 2;
}
.permit-workspace__notes {
grid-column: -2 / -1;
}In LTR, 1 / 2 is the leftmost column and -2 / -1 is the rightmost. In RTL, 1 / 2 is the rightmost column and -2 / -1 is the leftmost. The queue remains at logical start and the notes remain at logical end.
Line numbers do not denote physical x-coordinates. Positive numbers count from the start edge of the explicit grid. Negative numbers count backward from its end edge. This means -1 is inline end, not “the right boundary.” In RTL horizontal writing, it is the left boundary.
Negative lines stop at the explicit grid#
Negative line numbers count from the end of the explicit grid. If auto-placement creates implicit columns beyond that declared template, -1 still identifies the explicit grid's end line. It does not chase the outer edge of every implicit track created afterward.
That distinction matters when a layout mixes explicit placement with grid-auto-columns:
.review-board {
display: grid;
grid-template-columns: repeat(3, minmax(10rem, 1fr));
grid-auto-flow: column;
grid-auto-columns: 12rem;
}If extra items generate implicit columns, an item placed against -1 is anchored to the end of the three-column explicit template. Inspect the Grid overlay to see both the explicit and implicit tracks before deciding the browser counted incorrectly. It counted the grid you declared, which is an awkward but defensible position.
Named lines explain intent better#
Numeric lines are compact, but named lines make logical meaning visible:
.review-layout {
display: grid;
grid-template-columns:
[queue-start] minmax(14rem, 18rem)
[queue-end content-start] minmax(0, 1fr)
[content-end];
}
.review-layout__queue {
grid-column: queue-start / queue-end;
}
.review-layout__content {
grid-column: content-start / content-end;
}The named lines still follow direction, but a reviewer no longer has to remember whether -2 represents the start of a trailing track or the end of a long day. Avoid physical names such as left-edge unless the edge is truly physical.
One shorthand deserves special care. Four-value grid-area uses this order: row start, column start, row end, column end. It is not the top-right-bottom-left order used by physical edge shorthands. Thinking in block start, inline start, block end and inline end makes it readable in both LTR and RTL.
Auto-placement begins at grid start#
Items without explicit placement use the auto-placement algorithm. The default grid-auto-flow: row fills cells across each row, then creates or moves to the next row as needed. “Across” begins at column start, so an RTL grid begins on the right.
<section class="review-cards" dir="rtl" lang="ar">
<article>مراجعة المخطط</article>
<article>مطابقة الواجهة</article>
<article>ملاحظات الموقع</article>
</section>.review-cards {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
}The first article occupies the rightmost cell, the second the middle cell and the third the leftmost cell. Their DOM order is unchanged. An Arabic reader begins at the right and meets the first item first.
With grid-auto-flow: column, the algorithm fills each column through the block axis before moving to the next column. The first column is still the inline-start column, on the right in RTL horizontal writing. The property changes the primary placement axis; it does not provide a secret physical-left mode.
Dense packing can change the visible sequence#
grid-auto-flow: dense lets a later, smaller item backfill a hole left by an earlier item that did not fit. This can reduce empty cells. It can also make a later DOM item appear visually before an earlier one.
For a decorative tile gallery where every item is independent, that may be acceptable. For a review queue, numbered procedure, message list or set of focusable cards, it is a bad trade. Keyboard navigation and speech continue to follow source order while the eye follows a densely packed two-dimensional arrangement.
The Grid accessibility guide on MDN calls out dense, explicit placement and order because they can separate visual order from DOM order. The Grid specification is firmer: placement and reordering must not substitute for correct source ordering.
Use ordinary sparse auto-placement when sequence matters. Empty space is often cheaper than an interface that reads like someone shuffled the permits to improve shelf utilization.
Visual placement does not rewrite source order#
Named areas can put a footer above a heading. Numeric lines can put the last link in the first cell. The order property can affect auto-placement and painting. None of those changes the default speech or sequential keyboard order.
Start with a DOM that makes sense as one linear document. This matters at narrow breakpoints, in non-visual presentation and whenever the grid stylesheet is unavailable. Then use Grid to create a two-dimensional arrangement that does not contradict that sequence.
For the permit workspace, a reasonable source order might be the page heading, queue, current plan and inspector notes. A desktop template can place the queue and notes beside the plan. On a narrow screen, letting the DOM collapse into one column should still produce a useful reading order without a second set of order rules.
If the desktop mockup demands a visual order that conflicts with the logical one, decide which order carries meaning before coding. Grid can satisfy the mockup either way. It cannot make two conflicting sequences both obvious to the user, despite having several properties with confident names.
Test interactive grids with a keyboard. Focus should move through controls in a path a person can follow, not jump from the visual end to the start because the items were assigned to decorative areas. No Ritla check listed for this page decides whether your grid's source order is meaningful, so this remains a manual review.
Alignment values have their own direction rules#
Grid alignment separates the inline and block axes:
| Property | What it aligns | Axis in horizontal writing | Effect of RTL |
|---|---|---|---|
justify-items | Items within their grid areas | Inline | start moves to the right |
justify-self | One item within its grid area | Inline | start moves to the right |
justify-content | The whole track grid within its container | Inline | start moves to the right |
align-items | Items within their grid areas | Block | start normally remains top |
align-self | One item within its grid area | Block | start normally remains top |
align-content | The whole track grid within its container | Block | start normally remains top |
Use start and end when alignment should follow the relevant logical axis:
.review-card {
display: grid;
justify-items: start;
align-items: start;
}
.review-card__status {
justify-self: end;
}In LTR, the card's normal content aligns left and the status aligns right. In RTL, those inline results swap. Both remain at block start, normally the top.
The physical values left and right do not express reading start and end. If justify-self: right is the actual requirement in both locale versions, keep it. If it means “align at reading start,” use start.
stretch, center, space-between and the other non-side-specific values do not become opposite keywords in RTL. Direction still affects where tracks and start/end edges are, but Grid does not turn center into a different philosophical position.
Remember that justify-content only becomes visually obvious when the track grid is narrower than its container and free inline space remains. If tracks already fill the container, changing it may appear to do nothing. This is a sizing result, not proof that RTL has been ignored.
Gaps mirror cleanly, physical edges do not#
gap, row-gap and column-gap describe space between tracks. They do not need RTL variants:
.permit-workspace {
display: grid;
row-gap: 1.5rem;
column-gap: 1rem;
}The column gap remains between adjacent columns when the column axis reverses. Padding and margins need more thought. Logical properties such as padding-inline-start and margin-inline-end follow direction; padding-left and margin-right remain physical.
The Grid specification makes an especially useful distinction for absolutely positioned children: grid placement is flow-relative, while top, right, bottom and left offsets are physical. A child can therefore move to a mirrored grid area and still be offset toward the old physical side.
Suppose a review stamp belongs at the trailing edge of its panel. In the English layout that edge is right:
Problem
.review-panel {
position: relative;
}
.review-panel__stamp {
position: absolute;
top: 0.75rem;
right: 0.75rem;
}The grid area may mirror in RTL, but the stamp remains physically right. If its role is inline end, use logical insets:
Better
.review-panel {
position: relative;
}
.review-panel__stamp {
position: absolute;
inset-block-start: 0.75rem;
inset-inline-end: 0.75rem;
}The LTR position stays top-right. In RTL it becomes top-left, the panel's inline end. This is only correct because the intended edge is trailing. If the stamp represents a marker on the physical right side of a plan, right was the right property all along.
Transforms, background positions, SVG coordinates and horizontal shadow offsets also keep their authored physical behavior. Grid will not mirror their pixels simply because the item containing them moved. R030 names direction-sensitive physical properties without an RTL override. The check identifies the physical assumption; your design determines whether it is a bug or a deliberate exception.
The RTL CSS guide covers those non-Grid properties in depth. Here, the important point is diagnostic: a mirrored track and an unmirrored offset can both be correct CSS while producing one very incorrect component.
Not every grid should mirror#
Application layout generally follows reading direction. A queue, content panel and tools rail have logical start and end roles, so mirroring them is useful.
Some grids represent physical space instead. A site plan, seating chart, map overlay, image annotation layer or scientific coordinate display may need west to remain on the physical left and east on the physical right in every locale. Mirroring the coordinate system would change the information, not merely its presentation.
For a genuinely fixed spatial grid, establish a local LTR coordinate direction and give Arabic text inside it its own RTL direction:
<section class="site-plan" dir="ltr" aria-labelledby="site-plan-title">
<h2 id="site-plan-title" dir="rtl" lang="ar">مخطط الموقع</h2>
<p class="site-plan__label" dir="rtl" lang="ar">مدخل المبنى</p>
</section>.site-plan {
display: grid;
grid-template-columns: repeat(4, 1fr);
}Now grid line 1 stays on the physical left for the coordinate layer, while the Arabic label has the base direction its text needs. This is an explicit boundary, not a blanket direction: ltr rule applied to the whole application.
Do not use this pattern merely because the English grid looked familiar. Ask whether columns represent reading order or real-world coordinates. A navigation rail should usually mirror. A floor plan should usually not. The word “usually” is doing legitimate work here, which is more than can be said for it in many project updates.
Track sizing can create RTL overflow#
Direction changes where tracks start. It does not make a three-column desktop template fit a narrow viewport, nor does it shorten Arabic labels.
This declaration has an easily missed minimum:
.review-layout {
display: grid;
grid-template-columns: 16rem 1fr 18rem;
}Outside minmax(), a flexible 1fr track has an automatic minimum. Content can contribute a minimum size that prevents the track from shrinking as far as the design expected. Fixed side tracks add their widths before the center track receives what remains.
When the center track is allowed to shrink to zero and its content can wrap, express that explicitly:
.review-layout {
display: grid;
grid-template-columns: 16rem minmax(0, 1fr) 18rem;
gap: 1rem;
}
.review-layout__content {
min-inline-size: 0;
}The Grid track-sizing rules define 1fr outside minmax() with an automatic minimum, and MDN's minmax() reference documents the minmax(0, 1fr) pattern. Setting the item minimum to zero can also prevent its own automatic minimum from holding the track open.
Do not apply these declarations blindly. A plan canvas, data table or fixed-size control may have a real minimum. If the three tracks cannot fit without making content unusable, change the template at a breakpoint:
@media (max-width: 52rem) {
.permit-workspace {
grid-template-areas:
"queue"
"plan"
"notes";
grid-template-columns: minmax(0, 1fr);
}
}The one-column template follows source order from top to bottom and works the same in LTR and RTL. No reversed mobile template is needed because the block axis has not changed.
If an item or track extends the page's scrollable width, R040 names document-level horizontal overflow. If text-bearing grid items visibly overlap, R041 names that separate result. Neither check says which track rule caused it, so inspect minimum contributions, fixed tracks, spans, physical offsets and unbreakable content before choosing a repair. The horizontal overflow guide gives the full page-level debugging process.
Responsive templates should preserve the linear story#
Grid makes it easy to assign new areas at each breakpoint. That freedom is useful when a wide desktop workspace becomes a narrow single-column page. It also makes it easy to maintain three visual arrangements whose relationship to source order nobody can explain without colored markers.
Start from the narrow, linear reading order in the DOM. At wider breakpoints, place those same regions into named areas without changing their semantic sequence. Under RTL, keep area names in logical start-to-end order unless the product design intentionally changes the relationship.
When an item disappears at a breakpoint, verify what fills its cell. An explicitly named empty area remains part of the template. Auto-placed items do not necessarily move into it unless their placement rules allow that. If you need a different layout, declare a different template instead of hoping Grid interprets absence as a design meeting.
Nested grids can establish separate direction boundaries. An Arabic page may contain an LTR code comparison or fixed spatial plan, while the outer application shell remains RTL. Inspect each grid container's computed direction; do not assume every descendant shares the root after local dir attributes are introduced.
Debug the grid that the browser built#
When the RTL layout is wrong, inspect the grid before adding a locale selector.
- Confirm the grid container's computed
direction.lang="ar"alone does not set it. - Open the browser's Grid overlay and locate column line
1and-1. - Distinguish explicit tracks from implicit tracks created by auto-placement.
- Inspect each item's resolved row and column lines, including negative indexes and spans.
- Check whether
grid-template-areaswas reversed in an RTL override. - Look for
grid-auto-flow: dense, nonzeroordervalues and explicit placements that alter visual order. - Inspect
justify-*andalign-*separately. They act on different axes. - Disable physical margins, offsets and transforms one at a time.
- Check track minimums and the intrinsic size of long content.
If line 1 is on the left in an Arabic application grid, trace the nearest direction boundary. The container may be inheriting LTR, have an explicit dir="ltr", or sit on a page where direction was declared only through a selector that does not apply. Do not repair that by swapping every numeric line. Fix the direction source first.
If the lines are correct but an item sits in the wrong area, inspect the placement declaration and template. If the item is in the right area but its contents hug the wrong edge, inspect alignment. If the contents align correctly but a badge floats outside, inspect positioning. Grid debugging becomes much faster when each layer is allowed only one crime at a time.
Test the mirror, the order and the narrow case#
Use the same DOM and stylesheet for the LTR and RTL fixtures. Change lang, dir and localized content, then verify these outcomes:
| Test | LTR expectation | RTL expectation |
|---|---|---|
Column line 1 | Left edge | Right edge |
| Start-side named area | Leftmost logical area | Rightmost logical area |
| First auto-placed card | Leftmost first cell | Rightmost first cell |
justify-self: start | Left of its area | Right of its area |
| Block-start alignment | Top | Top |
| Physical coordinate grid | Fixed by its local direction | Same fixed coordinates |
Then test what a screenshot cannot settle:
- Tab through every interactive item and compare focus movement with the visible arrangement.
- Disable Grid in DevTools and confirm the source order still tells a coherent story.
- Resize through every template breakpoint and watch when implicit tracks appear.
- Add a long Arabic heading, a large fixed child and an unbreakable token separately.
- Check for document scrolling at the narrowest supported width and increased text size.
- Toggle the grid container's
dirdirectly to confirm which behavior is direction-driven. - Test hidden and dynamically inserted items, especially when dense packing is enabled.
For the permit workspace, the queue should occupy reading start, the plan should remain central and notes should occupy reading end. That statement is more useful than “the panels should swap,” because it still tells you what to expect when one panel disappears or the layout becomes a single column.
Let Grid mirror meaning, not coordinates#
An RTL grid does not need a second copy of every template. Give the container the correct direction, keep areas and tracks in logical start-to-end order, and let line numbering and auto-placement follow the inline axis. Use a local fixed direction only when the grid represents physical coordinates rather than reading order.
A free Ritla scan can surface direction-sensitive physical properties without an RTL override R030, document-level horizontal overflow R040 and visible text-bearing elements overlapping R041. Named-area intent, dense visual reordering and the meaning of a spatial grid still need the tests above.
If Grid is mirroring correctly but surrounding styles still assume physical left and right, continue with the RTL CSS guide. The best RTL grid is not the one with the largest override block. It is the one that understood what “first” meant.