CSS logical properties for RTL
Use CSS logical properties to make spacing, sizing, borders and positioning follow LTR and RTL flow without maintaining mirrored stylesheets.
Guide16 min read
An LTR component often reaches Arabic with a quiet collection of assumptions: space goes on the left, the badge belongs on the right, the accent border is also on the left, and apparently nobody thought these decisions would ever be questioned.
CSS logical properties let you describe those choices relative to content flow. Instead of margin-left, you can use margin-inline-start. In English, inline start is left. In Arabic, inline start is right. One declaration follows both directions without an RTL override and the small archaeological expedition required to understand it later.
Logical properties are not a command to replace every mention of left, right, top and bottom. The real question is simpler: should this side follow the writing mode, or should it stay physically fixed?
Answer that first. The property name usually follows.
Start with inline and block axes#
Physical CSS names sides of the screen: top, right, bottom and left. Logical CSS names positions in the flow of content.
For the horizontal writing mode used by English and Arabic interfaces:
| Logical position | LTR mapping | RTL mapping |
|---|---|---|
| inline start | left | right |
| inline end | right | left |
| block start | top | top |
| block end | bottom | bottom |
The inline axis follows the direction of a line of text. The block axis follows the direction in which lines stack. In a typical Arabic page using writing-mode: horizontal-tb, lines run right to left and blocks still stack from top to bottom.
.maintenance-card {
margin-inline-start: 1rem;
padding-inline-end: 1.25rem;
border-block-end: 1px solid #d8d4cc;
}On an LTR page, the margin sits on the left and the inline-end padding sits on the right. Under dir="rtl", those inline sides swap while the bottom border remains at block end.
MDN's logical-properties overview explains the axes and their writing-mode-relative mappings. The CSS Logical Properties specification defines the underlying property groups. The specification is a working draft, because even directions require paperwork.
Translate design intent, not property names#
A mechanical search-and-replace from left to inline-start can move an element to the wrong side in LTR. You need to preserve what the original design does before making it adapt.
Suppose a maintenance task card places its status badge at the top-right corner in English. In Arabic, the badge should move to the top-left because it belongs at the trailing edge of the card.
Problem
.task-card__status {
position: absolute;
top: 0.75rem;
right: 0.75rem;
}The badge stays physically right in both directions.
A rushed conversion sometimes replaces right with inset-inline-start. That is wrong. Inline start is left in English, so the conversion changes the LTR design before anyone even reaches the Arabic route. A fix that breaks the original layout is not internationalization. It is relocation.
Better
.task-card__status {
position: absolute;
inset-block-start: 0.75rem;
inset-inline-end: 0.75rem;
}inset-inline-end maps to right in LTR and left in RTL. The badge keeps its original English position and mirrors only when the component direction changes.
Write down the intent in ordinary language before converting a rule: “top trailing corner,” “space before the label,” “accent on the reading start,” or “physically attached to the viewport's left edge.” This is less glamorous than a codemod and much less likely to move every icon to the wrong side.
Replace directional margins and padding#
Margins and padding are the easiest place to begin because their logical forms are direct:
| Physical property | Logical property when flow-relative |
|---|---|
margin-left | margin-inline-start or margin-inline-end |
margin-right | margin-inline-end or margin-inline-start |
margin-top | margin-block-start |
margin-bottom | margin-block-end |
padding-left | padding-inline-start or padding-inline-end |
padding-right | padding-inline-end or padding-inline-start |
padding-top | padding-block-start |
padding-bottom | padding-block-end |
The first four rows are not a blind mapping table. Whether physical left means logical start or logical end depends on the design. A leading marker uses start; a trailing action uses end. CSS cannot infer which story your pixels were trying to tell.
Consider a dispatch card with a colored marker before its label.
Problem
.dispatch-card__label {
border-left: 0.25rem solid #c75b39;
padding-left: 0.75rem;
}Better
.dispatch-card__label {
border-inline-start: 0.25rem solid #c75b39;
padding-inline-start: 0.75rem;
}The marker and its breathing room move together. In LTR they remain on the left, preserving the original geometry. In RTL they move to the right, where reading begins.
Use axis shorthands when both sides share a rule:
.task-card {
margin-block: 0 1rem;
padding-inline: 1rem;
}margin-block accepts block-start followed by block-end. padding-inline: 1rem applies the same padding to inline start and end. The familiar four-value margin and padding shorthands remain physical in top-right-bottom-left order. They do not become philosophical when the document becomes RTL.
MDN's guide to logical margins, borders and padding lists the mappings and axis shorthands.
Use logical borders and corner radii#
Directional accent borders should follow the same intent as spacing:
.inspection-note {
border-inline-start: 0.3rem solid #6356c7;
padding-inline-start: 0.875rem;
}Borders on both inline sides can use border-inline; top and bottom in a horizontal writing mode can use border-block-start and border-block-end.
Corner radii require two logical positions because a corner belongs to both axes:
.record-panel {
border-start-start-radius: 1rem;
border-end-start-radius: 1rem;
}In horizontal LTR, those are the top-left and bottom-left corners. In horizontal RTL, they become top-right and bottom-right. The panel remains rounded on its inline-start edge.
Read the names as coordinates:
border-start-start-radiusmeans block start plus inline start;border-start-end-radiusmeans block start plus inline end;border-end-start-radiusmeans block end plus inline start;border-end-end-radiusmeans block end plus inline end.
The names are not charming. They are, however, precise, and precision is what we ask from CSS after it has already given us !important.
Do not convert a radius that represents a physical object. A map sheet, device frame or image crop may need the same physical corners in both locales. Logical radii are for corners whose meaning follows content flow.
Position with logical inset properties#
Absolute positioning is where physical assumptions become visible, often several breakpoints after the rule was written.
Use these equivalents when the position follows flow:
| Physical property | Logical property |
|---|---|
left | inset-inline-start or inset-inline-end |
right | inset-inline-end or inset-inline-start |
top | inset-block-start |
bottom | inset-block-end |
Again, left is not automatically start. A dismiss action at the trailing edge of an English panel uses right; its logical replacement is inset-inline-end.
.panel__dismiss {
position: absolute;
inset-block-start: 1rem;
inset-inline-end: 1rem;
}The positioned element still needs a predictable containing block:
.panel {
position: relative;
}Logical inset fixes the chosen side; it does not repair a missing containing block, an undersized panel or a button that overlaps a long Arabic heading. CSS remains unwilling to solve unrelated problems for free.
Use inset-inline or inset-block when setting both edges on an axis:
.panel__separator {
position: absolute;
inset-inline: 1rem;
inset-block-end: 0;
}MDN's guide to logical positioning covers inset properties and flow-relative values.
Size components by axis when the axis matters#
width and height are physical dimensions. Their logical counterparts describe the inline and block axes:
| Physical size | Logical size |
|---|---|
width | inline-size |
min-width | min-inline-size |
max-width | max-inline-size |
height | block-size |
min-height | min-block-size |
max-height | max-block-size |
For ordinary horizontal Arabic and English layouts, inline-size still maps to width and block-size to height. The value appears unchanged, so the conversion can feel ceremonial. It becomes useful when a component also supports vertical writing modes or when its API is intentionally flow-relative.
.maintenance-summary {
max-inline-size: 42rem;
min-block-size: 12rem;
}Do not convert every width merely to improve the logical-property percentage in a code audit. A video player with a physical aspect ratio, a map canvas or a viewport measurement may be clearer with physical dimensions. Use the name that describes the constraint.
The same restraint applies to aspect-ratio. It describes the physical ratio between width and height, not inline and block semantics. Logical CSS is a vocabulary, not a loyalty test.
Align text and floats by flow#
Arabic body text should normally inherit its alignment from the RTL direction. When a component explicitly sets alignment, prefer start and end for flow-relative intent:
.task-card__description {
text-align: start;
}
.task-card__secondary {
text-align: end;
}start maps to left in LTR and right in RTL. end does the reverse. R022 reports text-align: left explicitly set on majority-Arabic text.
Do not add text-align: start to every element by reflex. The initial behavior already follows direction in many ordinary blocks. An explicit declaration is useful when replacing a physical alignment or documenting a component contract, not as confetti.
Floats also accept flow-relative values:
.manual-figure {
float: inline-start;
margin-inline-end: 1rem;
}The figure floats at the reading start and keeps its separating margin on the opposite side. R031 reports float: left or float: right on content elements in RTL context without an override.
Floats are still floats. Logical values make their direction adaptive; they do not make float layout the best choice for application chrome. Use grid or flexbox when you are arranging interface components rather than wrapping prose around an illustration.
Flexbox and grid are partly logical already#
Flexbox and grid use many flow-relative concepts. justify-content: start, align-items: start, grid line starts and gaps can adapt without replacing left and right properties.
.task-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}With the correct document direction, a normal flex row begins from the inline start. In a dir="rtl" page, the first item sits at the right edge. You do not need row-reverse merely to make a row look RTL. That reverses the visual arrangement again and can make keyboard order fight the screen.
Logical properties still matter inside these layouts. A grid item may need margin-inline-start, a badge may need inset-inline-end, and a card may need border-inline-start. Switching to grid does not grant immunity from physical CSS. It merely gives the problem nicer diagrams.
Keep DOM order aligned with reading and task order. Use direction and logical layout to place items, not CSS order values to repaint an inconvenient DOM. The RTL CSS guide covers flex, grid, transforms and broader layout behavior in more depth.
Logical properties use the nearest direction context#
Logical properties do not consult the page locale once and keep that answer forever. They resolve against the writing mode and direction of the element's own context.
Consider an LTR code viewer nested inside an Arabic page:
<section class="diagnostic" dir="ltr">
<pre><code>GET /maintenance/records</code></pre>
</section>.diagnostic {
padding-inline-start: 1rem;
border-inline-start: 0.2rem solid #26705c;
}Inside that LTR boundary, inline start is left, even though the root page is RTL. This is correct. The component asked for spacing and a border at the start of its own content flow.
This can surprise tests that inspect only the root dir. When a logical property maps to an unexpected physical side, walk up the rendered DOM and find the nearest direction or writing-mode boundary. A nested dir="ltr", a third-party widget or a portalled subtree may be establishing the context. The browser is not ignoring the root. It found a closer instruction, as browsers tend to do when given several.
Component previews should test nested contexts deliberately: RTL component in LTR page, LTR component in RTL page, and inherited direction with no local override. A component that works only when mounted directly under html is less reusable than its folder name suggests.
Keep physical properties when the side is physical#
Logical properties are the default for flow-relative design, not a ban on geography.
Use physical sides when the requirement is genuinely physical:
- a map control fixed to the geographic west side of a map;
- a light-source shadow that should continue falling toward the same screen edge;
- a video timeline whose geometry follows media rather than language;
- an image crop anchored to a particular physical corner;
- a decorative background tied to artwork composition;
- a hardware diagram where left and right name real parts.
Write a short comment when the exception could look accidental:
.map-attribution {
/* Physical left matches the map provider's required placement. */
left: 0.5rem;
bottom: 0.5rem;
}Not everything should mirror. The guide to what should be mirrored in RTL helps separate directional meaning from physical or universal geometry.
Be particularly careful with top and bottom. They do not swap between horizontal LTR and RTL, so converting them to block properties does not change the Arabic layout. The logical names may still better express intent or support vertical writing modes, but they are not an RTL fix by themselves.
Know which effects remain physical#
Several direction-sensitive effects do not gain logical behavior merely because nearby spacing does:
transform: translateX(...)moves along the physical x-axis;box-shadowx-offsets remain physical;- gradient angles and background positions use physical geometry;
- SVG coordinates and paths do not mirror automatically;
- raster images keep their authored pixels;
- JavaScript measurements such as
leftandrightfromgetBoundingClientRect()are physical.
If one of these effects should mirror, add an explicit direction-aware rule or use a custom property whose sign changes under :dir(rtl).
.status-popover {
--shadow-x: 0.35rem;
box-shadow: var(--shadow-x) 0.5rem 1.25rem rgb(28 25 23 / 18%);
}
.status-popover:dir(rtl) {
--shadow-x: -0.35rem;
}R032 reports direction-sensitive shadow x-offsets without an RTL counterpart. R033 reports translateX moving an element off-screen in RTL. Logical margins will not negotiate with either effect. They have separate contracts and, apparently, separate legal counsel.
Avoid mirroring whole components with scaleX(-1). Text, icons and images flip with the container, forcing you to flip descendants back and producing a stack of transformations nobody wants to explain during an incident. Mirror the directional asset or movement that needs it.
Do not mix physical and logical declarations casually#
Logical and physical properties can map to the same used side. When both set that side, the cascade and source order decide which declaration wins.
.task-card {
margin-left: 2rem;
margin-inline-start: 1rem;
}In LTR, both target the left margin. In RTL, margin-inline-start targets the right margin while margin-left remains on the left, so the component now has two margins. Perhaps this was intended. The code offers no useful testimony.
During migration, avoid leaving both forms in the same rule unless you are providing a deliberate fallback. If legacy browser support requires one, keep the fallback adjacent, document the support target and test both parsing paths. Do not scatter physical declarations through one file and logical overrides through another.
Shorthands can also reset longhands. A later margin: 0 can clear margins established through logical mappings, and a later logical longhand can alter one mapped side. Inspect the final cascade rather than assuming logical properties live in a separate, more civilized layer.
Browser DevTools may show both the authored logical property and its physical effect. Check the Styles pane for the winning declaration and the Computed pane for the resulting sides. The difference between “declared correctly” and “won the cascade” has funded many debugging sessions.
Add legacy fallbacks as one removable block#
Current browser engines support the core logical margin, padding, border, sizing and inset properties used in this guide. Your support matrix may still include an older embedded webview, a long-lived kiosk or a browser that has been kept alive by contractual obligation and a power cable.
Check the exact properties you use against the browsers you support. Do not assume that support for margin-inline-start proves support for every newer logical value. Corner radii, floats and less common shorthands may have different histories in the environment that matters to you.
If a fallback is necessary, keep the physical version and its RTL override together, then replace both inside a feature query:
.task-card__status {
right: 0.75rem;
}
[dir="rtl"] .task-card__status {
right: auto;
left: 0.75rem;
}
@supports (inset-inline-end: 0.75rem) {
.task-card__status {
right: auto;
left: auto;
inset-inline-end: 0.75rem;
}
}Modern browsers use the logical inset. Older browsers keep the explicit physical pair. Resetting both left and right inside the feature query matters; otherwise a fallback can remain active on the opposite side and stretch or over-constrain the positioned element.
This fallback assumes the card follows the document direction. A legacy component with its own nested dir boundary needs scoped physical overrides for that boundary too. Logical properties remove that bookkeeping in supporting browsers, which is one of the reasons you are doing this in the first place.
This is fallback code, not the preferred final architecture. Give it a comment naming the legacy target and an expiry condition. Once that target leaves the support matrix, remove the entire block rather than preserving it as a historical exhibit.
Avoid adding a fallback by placing right immediately before inset-inline-end with no RTL override. A browser that ignores the logical declaration will keep the badge on the right in Arabic. The syntax degrades gracefully; the design does not.
Migrate a codebase by risk, not alphabetically#
A sensible migration starts with rules that encode directional intent:
- Search for
left,right,margin-left,margin-right,padding-left,padding-right, directional borders, floats and text alignment. - Classify each use as flow-relative or genuinely physical.
- Convert one component and preserve its LTR geometry.
- Render the same state under
dir="rtl". - Check nested direction, narrow widths and long Arabic content.
- Remove redundant RTL overrides after the logical rule replaces them.
- Continue through shared components before one-off pages.
Do not begin with a global replacement. right: 0 may mean trailing edge, leading edge in an RTL-only component, or the physical right side of a diagram. A codemod can change syntax. It cannot interview the designer who left the company.
Prioritize positioned controls, fixed-width toolbars, badges, decorative accents and components already carrying [dir="rtl"] overrides. These are more likely to produce overlap or horizontal overflow than a harmless physical margin in a static illustration.
R030 reports direction-sensitive physical properties without an RTL override. Use the report as a review queue, not proof that every physical declaration is wrong. The rule names the risk; your component intent decides the repair.
Put logical intent into design tokens and component APIs#
A migration will not hold if new code keeps reintroducing physical concepts through design-system props.
Prefer semantic tokens:
:root {
--space-content-start: 1rem;
--space-action-end: 0.75rem;
}
.task-card {
padding-inline-start: var(--space-content-start);
padding-inline-end: var(--space-action-end);
}Avoid renaming every --space-left token to --space-start without checking what it represents. A token tied to a map overlay may be physically left. A token for space before a label is probably inline start. Tokens, like properties, should describe intent rather than participate in a branding exercise.
Component APIs should prefer start and end for flow-relative placement and use explicit names such as physicalLeft only when that behavior is genuinely offered. Document which direction context the component follows, especially if it creates an internal dir boundary.
For CSS-in-JS, map logical prop names directly to CSS logical properties. Avoid a runtime helper that reads the global locale and emits marginLeft or marginRight; it can become stale during locale switches and fails inside nested direction contexts. Let the browser resolve the logical property from the actual rendered context. It is already there and does not need another meeting.
Let direction changes remap without rebuilding styles#
A logical declaration is resolved from the current writing mode and direction. If a single-page app changes the root from dir="ltr" to dir="rtl", the browser remaps margin-inline-start, inset-inline-end and the other logical properties without the application rebuilding every style object.
That is an important difference from direction-specific JavaScript:
// isRtl is the app's current locale-direction flag; it can become stale.
const style = {
marginLeft: isRtl ? 0 : "1rem",
marginRight: isRtl ? "1rem" : 0
};The object captures one direction at the moment it is created. A cached component, memoized value or portalled element may keep those physical properties after the locale changes.
Prefer a stable class:
.task-card {
margin-inline-start: 1rem;
}Now the rendered dir context is the source of truth. Test the locale switch without reloading and inspect existing nodes, not only newly mounted ones. If old nodes move correctly, the browser is doing the mapping. If only new nodes are correct, application state is still manufacturing physical sides somewhere.
Pseudo-elements benefit from the same approach. A generated accent can attach to flow start without an RTL duplicate:
.task-card::before {
content: "";
position: absolute;
inset-block: 0;
inset-inline-start: 0;
inline-size: 0.25rem;
background: #c75b39;
}The originating card needs a positioning context, and its content needs enough inline-start padding to clear the accent. Logical positioning handles the side; it does not prevent the decoration from covering text. CSS has agreed to map the edge, not supervise the composition.
Directional icons remain a separate asset decision. A logical inset can move a chevron to the correct edge, but it cannot make the chevron point along the sequence it advances. Position and meaning occasionally travel together. They are still different tickets.
Use logical scroll spacing, but test scroll behavior separately#
Scroll snap and anchored navigation can also carry directional spacing. scroll-padding-inline-start reserves room at a scroll container's reading start, while scroll-margin-inline-start adds space before a target along the inline axis.
.maintenance-rail {
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-padding-inline-start: 1rem;
}
.maintenance-rail__item {
scroll-snap-align: start;
scroll-margin-inline-start: 1rem;
}The spacing can follow LTR and RTL flow, but overflow-x, transforms and JavaScript scroll coordinates still involve physical axes and browser RTL scroll conventions. In the Chromium behavior supplied for this brief, an RTL scroll container has scrollLeft equal to zero at its start and negative values toward its end.
Do not rewrite a scroll bug by swapping every positive number for a negative one. Test the start, middle and end positions in the browsers you support, and prefer APIs or calculations that explicitly account for direction. Logical scroll spacing solves spacing. It does not reinterpret an old carousel algorithm out of professional courtesy.
If a child overhangs the page after conversion, inspect the whole bounding box and the side of the overhang. The horizontal-overflow guide covers document-level diagnosis in depth.
Test the mapping, not just the screenshot#
Visual comparison is useful, but computed styles tell you whether the component adapted for the reason you intended.
// page and expect are supplied by the browser test runner.
const card = page.locator("[data-test=maintenance-card]");
await expect(card).toHaveCSS("direction", "rtl");
await expect(card).toHaveCSS("margin-right", "16px");
await expect(card).toHaveCSS("margin-left", "0px");This fixture assumes the card declares margin-inline-start: 1rem and the test environment uses a 16px root font size. Under RTL, inline start maps to the right, so the physical computed sides reveal the mapping.
Run a paired LTR fixture and expect the same 16px margin on the left. Then test the component inside a nested LTR boundary on the Arabic page. The nearest direction context should win.
Add interaction and layout checks where the component warrants them:
- keyboard focus follows DOM order rather than a visually reversed order;
- positioned actions do not overlap long Arabic headings;
- logical padding does not combine with a forgotten physical fallback;
- the page has no document-level horizontal overflow;
- text alignment follows the intended direction;
- shadows, transforms and directional icons receive explicit handling;
- resize and zoom do not expose a side that only worked at one width.
R040 reports document-level horizontal overflow. If the page gains a horizontal scrollbar after conversion, inspect positioned descendants and mixed physical/logical declarations before blaming Arabic text for being long. The text is merely occupying the space it was promised.
Debug from the used side back to the intent#
When a logical property lands on the wrong side:
- Inspect the element's computed
directionandwriting-mode. - Find the nearest ancestor or host that establishes either value.
- Translate the logical property into its physical side for that context.
- Check whether a physical declaration maps to the same side and wins later.
- Verify that start or end matches the component's actual design intent.
- Compare the original LTR geometry before changing the logical keyword.
- Test the same component in the opposite and nested directions.
The fifth step catches an especially common mistake: using start because the original property said left. The original side is evidence, not intent. A trailing English control on the right becomes inline end, not inline start.
Do not add an RTL override until you know why the logical declaration failed. The problem may be the cascade, a nested LTR boundary, an incorrect containing block or a property with no logical equivalent. An override can hide all four with admirable efficiency and leave them for the next person.
Let the browser map the flow#
Logical properties work when the CSS names the relationship you actually care about: before or after content, start or end of a line, start or end of a block. Physical properties remain appropriate when the screen side itself carries meaning.
That is the useful rule. Not “never write left.” Not “convert everything by Friday.” Describe flow with logical CSS, describe geometry with physical CSS, and make the exception clear enough that the next engineer does not helpfully undo it.
A Ritla scan can surface direction-sensitive physical properties and overflow after the Arabic page renders. Continue with the RTL CSS guide for layout systems and transforms, or the horizontal-overflow guide when a misplaced edge has already widened the page.
Checks in this guide
- R022text-align: left explicitly set on majority-Arabic text
- R031float: left/right on content elements in RTL context without override
- R032Direction-sensitive shadow x-offsets without an RTL counterpart
- R033translateX moves the element off-screen in RTL
- R030Direction-sensitive physical properties without an RTL override
- R040Document-level horizontal overflow