CSS for RTL — Advanced CSS Techniques for Bidirectional Text Support
In this tutorial, you will learn about CSS for RTL. We cover key concepts, practical examples, and best practices to help you master this topic.
CSS techniques for RTL support include logical properties, CSS logical values, auto-flipping transforms, and direction-aware layout systems.
What You'll Learn
By the end of this tutorial, you'll understand CSS logical properties and values in depth, how to use CSS logical shorthands, how to handle RTL-specific styling without overrides, and how CSS Grid and Flexbox naturally support direction changes.
Why It Matters
Physical CSS properties (left, right, margin-left) require duplicating stylesheets or writing complex overrides for RTL support. CSS logical properties adapt automatically to the document direction. Mastering logical CSS reduces RTL maintenance by 90% and eliminates the bug-prone pattern of [dir="rtl"] .class { ... } overrides.
Real-World Use
A design system team converts their component library from physical to logical CSS properties. The button component, previously requiring 8 lines of RTL overrides, now works in both directions with zero direction-specific code. Adding RTL support for a new component takes minutes instead of hours.
Logical Properties Deep Dive
/* styles/logical-properties.css — Comprehensive logical property reference */
/*
Physical Logical (horizontal) Logical (vertical)
margin-left margin-inline-start margin-block-start
margin-right margin-inline-end margin-block-end
padding-left padding-inline-start padding-block-start
padding-right padding-inline-end padding-block-end
border-left border-inline-start border-block-start
border-right border-inline-end border-block-end
left inset-inline-start inset-block-start
right inset-inline-end inset-block-end
top
bottom
text-align: left text-align: start
text-align: right text-align: end
float: left float: inline-start
float: right float: inline-end
*/
/* Shorthand properties */
.element {
/* margin: top right bottom left */
/* margin-block: top bottom */
/* margin-inline: right left */
margin: 20px 16px 30px 16px;
/* Same as: */
margin-block: 20px 30px;
margin-inline: 16px;
padding: 10px 24px;
/* Same as: */
padding-block: 10px;
padding-inline: 24px;
border: 2px solid #4a90d9;
border-width: 2px 0 2px 2px;
/* Same as: */
border-block-width: 2px;
border-inline-width: 2px 0;
}
/* Border radius */
.box {
border-radius: 8px 0 0 8px;
/* top-left, top-right, bottom-right, bottom-left */
/* Logical: */
border-start-start-radius: 8px;
border-start-end-radius: 0;
border-end-end-radius: 0;
border-end-start-radius: 8px;
/* Shorthand — same order works for both LTR and RTL */
/* In LTR: top-left & bottom-left have 8px */
/* In RTL: top-right & bottom-right have 8px (it just works!) */
}
Direction-Aware Transformations
/* styles/rtl-transforms.css — Auto-flipping elements */
/* Option 1: CSS transform with logical direction */
.directional-element {
transform: scaleX(1); /* Normal in LTR */
transition: transform 0.2s;
}
[dir="rtl"] .directional-element {
transform: scaleX(-1); /* Flipped in RTL */
}
/* Option 2: Using CSS custom properties for direction */
:root {
--direction-scale: 1;
}
[dir="rtl"] {
--direction-scale: -1;
}
.icon-arrow {
transform: scaleX(var(--direction-scale));
}
/* Option 3: Animation direction */
@keyframes slide-in {
from {
transform: translateX(calc(var(--direction-scale, 1) * -100%));
}
to {
transform: translateX(0);
}
}
/* Option 4: Progress bars and range sliders */
[dir="rtl"] .progress-bar {
transform: scaleX(-1); /* Fill from right to left */
}
[dir="rtl"] input[type="range"] {
transform: scaleX(-1); /* Higher value = more filled from right */
}
Advanced Grid and Flexbox for RTL
/* styles/rtl-grid-flex.css — Direction-aware layouts */
/* Grid: auto-placement respects direction */
.grid-gallery {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
/* Items flow left-to-right in LTR, right-to-left in RTL */
/* No changes needed — it's automatic! */
}
/* Grid with explicit placement — use logical grid properties */
.grid-layout {
display: grid;
grid-template-columns: 200px 1fr 300px;
grid-template-rows: auto 1fr auto;
gap: 16px;
/* Physical properties (break in RTL) */
grid-column: 1; /* Always first column, but "first" changes in RTL! */
/* Logical properties work correctly */
grid-column-start: 1; /* First column in LTR, first column from left in RTL */
grid-column-end: 2;
}
/* Better: use named grid areas (direction-agnostic) */
.grid-layout {
grid-template-areas:
"header header header"
"sidebar main aside"
"footer footer footer";
}
.sidebar { grid-area: sidebar; }
.main { grid-area: main; }
.aside { grid-area: aside; }
/*
In LTR: sidebar | main | aside
In RTL: aside | main | sidebar
YES — grid areas flip automatically!
*/
/* Flexbox auto-flips in RTL */
.flex-nav {
display: flex;
gap: 24px;
/* In LTR: Home | About | Contact */
/* In RTL: Contact | About | Home */
/* Automatic! */
}
/* But watch out for flex order overrides */
.flex-nav li:nth-child(1) { order: 3; } /* Avoid — order doesn't flip */
RTL-Specific Typography
/* styles/rtl-typography.css — Typography adjustments for RTL */
/* Different font families for different scripts */
body {
font-family: 'Inter', system-ui, sans-serif;
}
[lang="ar"] body {
font-family: 'Noto Naskh Arabic', 'Traditional Arabic', serif;
}
[lang="he"] body {
font-family: 'Noto Sans Hebrew', 'Arial Hebrew', sans-serif;
}
/* Font size adjustments — Arabic text may need larger sizes */
[lang="ar"] body {
font-size: 1.05em; /* Arabic script is often smaller at the same px size */
line-height: 1.8; /* Taller ascenders/descenders need more line height */
}
/* Number alignment in tables — use text-align: end */
table td.amount {
text-align: end;
/* Right in LTR, left in RTL — numbers are still left-aligned in RTL */
/* BUT: financial numbers in Arabic contexts are still LTR */
}
/* Better: use unicode-bidi for mixed numbers */
table td.amount {
direction: ltr; /* Numbers always LTR even in RTL context */
text-align: end;
unicode-bidi: embed;
}
/* Text decoration positioning */
[dir="rtl"] u,
[dir="rtl"] .underline {
text-underline-position: under; /* Better for Arabic descenders */
}
/* Line height adjustments for Arabic */
p {
line-height: 1.6;
}
[lang="ar"] p {
line-height: 1.8; /* Arabic needs more line height due to ascenders */
}
CSS Custom Properties for Direction
/* styles/direction-vars.css — Using CSS variables for direction control */
:root {
/* Default LTR values */
--start: left;
--end: right;
--inline-start: 0;
--inline-end: auto;
--translate-direction: 1;
--progress-direction: 1;
--arrow-transform: none;
}
[dir="rtl"] {
--start: right;
--end: left;
--inline-start: auto;
--inline-end: 0;
--translate-direction: -1;
--progress-direction: 1;
--arrow-transform: scaleX(-1);
}
/* Usage */
.tooltip {
/* Position tooltip consistently */
left: var(--inline-start);
right: var(--inline-end);
}
.slide-in {
animation: slide 0.3s ease;
}
@keyframes slide {
from {
transform: translateX(calc(-100% * var(--translate-direction)));
}
to {
transform: translateX(0);
}
}
.progress-fill {
transform: scaleX(var(--progress-direction));
transform-origin: var(--start);
}
.nav-arrow {
transform: var(--arrow-transform);
}
Common Mistakes
- Using
[dir="rtl"]overrides as a crutch. One override per component is a sign of using physical properties. Zero overrides per component is the goal with logical properties. Aim to eliminate direction-specific CSS entirely. - Forgetting about transforms and animations. A slide-in animation from the left in English should slide from the right in Arabic. Use CSS custom properties for direction so animations naturally flip.
- Not testing with real RTL content in all breakpoints. RTL layout may work on desktop but break on mobile. Test RTL in responsive mode with actual Arabic or Hebrew text, not Latin placeholder text.
- Applying direction to elements that should remain LTR. Code snippets, URLs, email addresses, and numbers should stay LTR even in an RTL context. Use dir="ltr" and unicode-bidi: embed for these elements.
- Overlooking absolute/fixed positioning. A fixed-position close button at top-right should be top-left in RTL. Use inset-inline-end: 20px instead of right: 20px.
Practice Questions
- How do CSS logical properties differ from physical properties?
- What is the difference between margin-inline-start and margin-left?
- How does CSS Grid's named areas automatically handle RTL?
- Why should transforms and animations use direction-aware values?
- How do you handle mixed-direction content (Arabic text with English numbers)?
Challenge: Convert a complete component library (button, card, navbar, sidebar, modal, form inputs, table, pagination) from physical to logical CSS properties. Measure the number of lines of RTL-specific CSS before and after. The goal is zero lines of [dir="rtl"] override CSS.
FAQ
{{< faq "Do I still need [dir=\"rtl\"] overrides?" "With logical properties, you should need very few or none. The main exceptions are: icon/transform flips, background-position adjustments, and some third-party widget styling." >}}
Mini Project
Build a fully bidirectional component library with: 10 components (button, card, navbar, sidebar, modal, input, select, table, pagination, breadcrumb) using only CSS logical properties — zero [dir="rtl"] overrides. Include direction-aware animations (slide-in, progress bars), proper typography for Arabic/Hebrew fonts, and a demo page showing each component rendered in both LTR and RTL simultaneously.
What's Next
You've mastered CSS for RTL. Next, learn about i18n Routing for URL-based locale routing strategies.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro