Building Layouts That Adapt to Language Direction
Multilingual websites introduce a challenge that goes beyond translation: layout direction. When content switches from English to Arabic, for example, text flows right-to-left, and the entire visual layout must follow. Setting lang and dir attributes on the HTML element is the foundation, but the real work is in the CSS.

We'll walk through the core techniques that make direction-aware styling manageable, using a landing page demo with English, Arabic, and Japanese versions as a reference.
Starting With the Right Markup
The lang and dir attributes on the root element define the document's language and direction. These attributes also matter for SEO and accessibility. The charset meta tag must be set to UTF-8, as it's the only valid encoding for HTML that supports all languages.
<meta charset="utf-8">
Use these attributes in CSS selectors to apply language-specific styles. The :lang() pseudo-class targets specific languages directly:
html:lang(ar),
html:lang(jp){
--offers-item-after-font-size: 1.2rem;
}
For the Japanese version, which uses vertical writing mode, adjust the writing-mode property accordingly:

html:lang(jp) .about__text {
writing-mode: vertical-rl;
}
CSS Custom Properties for Easy Theming
Direction changes often require swapping physical properties like left and right. Rather than writing verbose overrides, assign values to CSS custom properties that can be re-assigned per language or direction:
html {
/* colors */
--dark-color: #161616;
--light-color: #eee;
--primary-text-color: var(--dark-color);
--primary-bg-color: #fff;
--shadow-color: var(--light-color);
--hero-bg-gradient: linear-gradient(90deg, #30333f, #161616, #161616);
/* font sizes */
--logo-font-size: 2rem;
--lang-switcher-font-size: 1.02em;
--offers-item-after-font-size: 1.5rem;
/* margin and padding */
--btn-padding: 7px;
--sec-padding-block: 120px;
/* height and width */
--hero-height: 500px;
--cta-img-width: 45.75%;
}
The same technique works for any toggleable state, like dark mode:
body {
background-color: var(--primary-bg-color);
color: var(--primary-text-color);
}
body.dark {
--primary-bg-color: #0f0f0f;
--primary-text-color: var(--light-color);
/* other changes */
--shadow-color: #13151a;
--hero-bg-gradient: linear-gradient(90deg, #191b20, #131313, #131313);
}

Pseudo-Classes and Attribute Selectors
The :lang() pseudo-class can change font sizes, families, and other properties for specific languages, reducing the need for duplicate rules. When a default font works for one script but not another, fallback fonts are only a partial solution—if the default font doesn't support the script, the fallback kicks in; if it does but the design needs a change, you're stuck.
A better approach is to combine custom properties with :lang():
html {
--font-family: 'Roboto', sans-serif;
}
html:lang(ar){
--font-family: 'Tajawal', sans-serif;
}
html:lang(jp){
--font-family: 'Noto Sans JP', sans-serif;
}
The attr() function inside content on pseudo-elements makes dynamic content possible based on HTML attribute values. This can be used with the dir attribute or any custom data attribute:
<div dir="ltr"></div>
<div dir="rtl"></div>
div::after {
content: attr(dir);
}

Custom data attributes like data-name offer the same flexibility:
<div dir="ltr"></div>
<div dir="rtl"></div>
div::after {
content: attr(data-name);
}

In the demo, JavaScript sets a data-offer attribute on each card with a value like "special offer" or "best offer," which CSS then styles:

<div class="offers__item relative" data-i18n_attr="special_offer">
<figure class="offers__item_img">
<img src="./assets/images/offer1.png" data-i18n_attr="image_alt" alt="" class="w-100">
</figure>
<div class="offer-content_item-text">
<p class="para" data-i18n="offer_item_text"></p>
<span class="price bolder" data-i18n="offer_item_price"></span>
</div>
</div>
.offers__item::after {
content: attr(data-offer);
/* etc. */
}
For direction-specific rules, the [dir='rtl'] attribute selector applies when the element itself or an ancestor carries that attribute.
Logical Properties Replace Physical Ones
Traditional physical properties (left, right, margin-top, etc.) require manual overrides when direction flips. CSS logical properties are direction-aware equivalents: inline runs parallel to the writing mode, block runs perpendicular, and both have start and end values.
| Writing Mode | x-axis | y-axis |
|---|---|---|
| horizontal | inline | block |
| vertical | block | inline |
For example, margin-inline-start automatically becomes margin-left or margin-right depending on the direction. Logical properties also cover borders:

ltr writing mode..footer {
border-start-end-radius: 120px;
}

These properties genuinely adapt. In the Japanese vertical writing mode, a block-level margin may appear as a left margin because the writing mode effectively rotates the layout 90 degrees:
/* The "About" section when langauge is Japanese */
html:lang(jp) .about__text {
margin-block-end: auto;
width: max-content;
}

Positioning Strategies That Survive Direction Changes
Absolute and fixed positioning are brittle across direction changes. The demo shows a footer newsletter form where the button is absolutely positioned inside a relatively positioned parent, and a hero background using an absolutely positioned ::before pseudo-element:

<form id="newsletter-form" class="relative">
<input type="email" data-i18n_attr="footer_input_placeholder" class="w-100">
<button class="btn btn--tertiary footer__newsletter_btn bolder absolute" data-i18n="footer_newsLetter_btn"></button>
</form>
html[dir="ltr"] .footer__newsletter_btn {
right: 0;
}
html[dir="rtl"] .footer__newsletter_btn {
left: 0;
}

rtl writing mode.<header class="hero relative">
<!-- etc. -->
</header>
.hero {
background-image: linear-gradient(90deg, #30333f, #161616, #161616);
}
.hero::before {
content: '';
display: block;
height: 100%;
width: 33.33%;
background-color: var(--primary-color);
clip-path: polygon(20% 0%, 100% 0, 100% 100%, 0% 100%);
position: absolute;
top: 0;
right: 0;
}

Problems multiply in RTL: the clip-path direction, background gradient, image orientation, and social widget position all need fixes. A simpler flip trick works when the hero has a patterned background: wrap content in two containers and rotate both 180 degrees:
<header class="hero relative">
<div class="hero__content">
<!-- etc. -->
</div>
<div class="hero__social absolute">
<div class="d-flex flex-col">
<!-- etc. -->
</div>
</div>
</header>
html[dir="rtl"] .hero,
html[dir="rtl"] .hero__content,
html[dir="rtl"] .hero__img img,
html[dir="rtl"] .hero__social > div {
transform: rotateY(180deg);
}

For smaller translations, transform: translate() offers a controlled alternative to repositioning. Store the offset value in a custom property and invert it per direction:
html {
--about-img-background-move: -20%;
}
html[dir='rtl']{
--about-img-background-move: 20%;
}

<figure class="about__img relative">
<img src="image.jpg" data-i18n_attr="image_alt" class="w-100">
</figure>
.about__img::after {
content: '';
position: absolute;
z-index: -1;
transform: translateY(-75%) translateX(var(--about-img-background-move));
/* etc. */
}
Margins vs. Transforms
Negative margins shift an element and collapse its original space, pulling subsequent content up. Transforms move an element visually while keeping its original space reserved in the document flow:
<section>
<div id="d1"></div>
<div id="d2"></div>
<div id="d3"></div>
</section>
div {
width: 100px;
height: 100px;
border: 2px solid;
}
#d1 {
background-color: yellow;
border-color: red;
}
#d2 {
background-color: lightblue;
border-color: blue;
}
#d3 {
background-color: green;
border-color: red;
}

#d2 {
margin-top: -40px;
margin-bottom: -70px;
}

When margins are direction-dependent, combine them with logical properties and custom properties instead of hard-coded values:
#d2 {
margin-top: -40px;
/* margin-bottom: -70px; */
}

#d2 {
/* margin-top: -40px;*/
transform: translateY(-40px);
}

Flexbox and Grid
Flexbox naturally handles alignment along the writing mode's main axis. Combined with logical properties and transforms, it reduces the need for positioned elements in many layouts—for example, centering the offer badges or laying out the hero section:
.offers__item::after {
content: attr(data-offer);
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}

<div class="d-lg-flex align-items-center">
<div class="hero__text d-xl-flex align-items-center">
<div>
<!-- text -->
</div>
</div>
<figure class="hero__img relative">
<img src="https://css-tricks.com/image.jpg" data-i18n_attr="image_alt" class="w-100">
</figure>
</div>

.hero__text {
width: 56.5%;
}
.hero__img {
width: 33.33%;
}
Negative margins within flexbox can also position content without absolute positioning. The call-to-action image is pulled in with a negative logical margin while the text flows around it:
<section class="cta d-xl-flex align-items-center">
<div class="cta__text w-100">
<!-- etc. -->
</div>
<figure class="cta__img">
<img src="image.jpg" data-i18n_attr="image_alt" class="w-100">
</figure>
</section>

Grid offers two-axis control that works well directionally. In the offers section, JavaScript appends more cards, and the grid container rearranges them responsively without media queries or direction-specific adjustments:
// offers section ==> "see all" btn functionality
(function(){
document.querySelector('.offers .btn').addEventListener('click', function(){
const offersContent = document.querySelector('.offers__content');
offersContent.innerHTML += offersContent.innerHTML;
offersContent.classList.remove('offers__content--has-margin');
this.remove();
})
})();
<div class="offers__content offers__content--has-margin d-grid">
<div class="offers__item relative" data-i18n_attr="special_offer">
<!-- etc. -->
</div>
<div class="offers__item relative" data-i18n_attr="best_offer">
<!-- etc. -->
</div>
<div class="offers__item relative" data-i18n_attr="best_offer">
<!-- etc. -->
</div>
</div>
html {
/* custom properties */
--offers-content-column: repeat(3, 1fr);
--offers-content-gap: 5vw;
}
.offers__content {
display: grid;
grid-template-columns: var(--offers-content-column);
gap: var(--offers-content-gap);
}
.offers__content--has-margin {
margin-block-end: 60px;
}

Putting It All Together
The final demo page uses a mobile-first approach with CSS custom properties as a central theme system. When a language change affects the layout—whether it's a font preference, a writing-mode switch, or a full RTL structure—the cascade takes care of the rest.
Direction-aware layouts don't require separate style sheets per language. The right combination of custom properties, pseudo-classes, logical properties, and modern layout tools keeps the CSS maintainable while supporting any script and direction.



