RTL Layout Adaptation Practices for Web Front-End Development
The guide outlines a direction‑based RTL adaptation workflow for web front‑ends—detecting language, setting the HTML dir attribute, using a lightweight helper, configuring postcss‑rtlcss (combined mode) to generate mirrored CSS, handling SSR and third‑party components, and providing a dev switch tool for seamless LTR/RTL support.
When expanding products to RTL (right‑to‑left) language markets such as the Middle East, the UI must be adapted so that the experience remains consistent for users who read from right to left.
RTL Layout Overview
The main visual differences between LTR and RTL layouts include text direction, the order of primary/secondary buttons, progress‑bar fill direction, and the orientation of navigation icons. Other icons remain unchanged.
Two Implementation Schemes
transform
Using the CSS transform property to flip the whole page horizontally:
transform: scaleX(-1);This flips the entire layout, but also flips text and non‑directional images. Elements that should not be mirrored must be flipped back individually, which can be labor‑intensive. The advantage is that no JavaScript changes are required.
direction
Using the CSS direction property (or the HTML dir attribute) to set the document direction:
dir="rtl"Setting direction: rtl on the html element changes the flow of text and layout. However, only a subset of properties are automatically mirrored (e.g., text-align , inline‑start , block‑start ), while many others ( margin , padding , float , etc.) remain unchanged and must be handled manually.
Choosing a Scheme
Because most UI components only need text direction changes and the amount of custom mirroring code would be high with transform , the direction ‑based approach was selected.
General Direction‑Based Adaptation
A utility library determines the language from the URL query string or cookies and sets the dir attribute on the html tag. It also provides setDirection and isRTL helpers.
import { Cookie } from '@music/helper';
import { parse } from '@music/mobile-url';
const rtlLngs = ['ar-EG', 'he_IL'];
export default class RTL {
private lng: string;
constructor(lng?: string) {
this.lng = lng || '';
if (typeof window !== 'undefined') {
const { location } = window as Window;
if (!this.lng) {
this.lng = (parse(location.search) as any).language || Cookie.get('language') || 'en-US';
}
document.documentElement.setAttribute('dir', rtlLngs.includes(this.lng) ? 'rtl' : 'ltr');
}
}
setDirection(lng?: string) {
this.lng = lng || '';
document.documentElement.setAttribute('dir', rtlLngs.includes(this.lng) ? 'rtl' : 'ltr');
}
static isRTL(lng?: string) {
if (lng) return rtlLngs.includes(lng);
if (typeof window !== 'undefined') {
const { location } = window as Window;
const l = (parse(location.search) as any).language || Cookie.get('language') || 'en-US';
return rtlLngs.includes(l);
}
return false;
}
}In server‑side rendering (SSR) the direction must be injected via Helmet because document is unavailable.
import { Helmet, HelmetProvider } from 'react-helmet-async';
function App({ isRTL }) {
return (
...
);
}PostCSS RTLCSS Plugin Configuration
During the build process, postcss-rtlcss (based on rtlcss ) converts CSS files to support both LTR and RTL. The recommended mode is Mode.combined , which generates separate rule sets for each direction.
import { postcssRTLCSS } from 'postcss-rtlcss';
import { Mode } from 'postcss-rtlcss/options';
const defaultOptions = {
mode: Mode.combined,
ignorePrefixedRules: true,
ltrPrefix: '[dir="ltr"]',
rtlPrefix: '[dir="rtl"]',
bothPrefix: '[dir]'
};
const options = {
...defaultOptions,
safeBothPrefix: true,
processUrls: true,
processKeyFrames: true,
useCalc: true
};
export default {
module: {
rules: [{
test: /\.css$/,
use: [
{ loader: 'css-loader' },
{ loader: 'postcss-loader', options: { postcssOptions: { plugins: [postcssRTLCSS(options)] } } }
]
}]
}
};Key options:
mode : Mode.combined creates both LTR and RTL rule sets.
safeBothPrefix : When true , adds the [dir] prefix to rules that should not be mirrored, preserving selector specificity.
processUrls : Transforms direction‑specific parts of URLs (e.g., ltr/arrow-left.png → rtl/arrow-right.png ).
ignorePrefixedRules : Skips conversion for selectors already containing ltrPrefix , rtlPrefix or bothPrefix .
useCalc : Converts values like background-position-x to calc(100% - value) for RTL.
processKeyFrames : Mirrors keyframe animations.
Avoid Inline Styles
Because the plugin only processes external CSS files, inline styles should be avoided. If inline styles are necessary, developers must handle RTL manually.
Third‑Party Library Adaptation
Libraries such as antd already support RTL via a direction prop. However, when using postcss-rtlcss , the library’s own RTL classes can clash with the generated selectors. The solution is to exclude the library’s CSS from the transformation.
import { ConfigProvider } from 'antd';
export default ({ isRTL }) => (
);For Swiper , setting dir="rtl" on an ancestor element is sufficient.
Quick RTL Switch Tool
A development‑time helper component renders a fixed‑position language selector that updates the language cookie and reloads the page with the new query parameter.
import React, { useCallback } from 'react';
import ReactDOM from 'react-dom';
import Select from 'antd/lib/select';
import { parse, stringify } from '@music/mobile-url';
import { Cookie } from '@music/helper';
const rtlLngs = ['ar-EG', 'he_IL'];
const i18nMap = { 'zh-CN': '简体中文', 'en-US': '英文', 'ar-EG': '阿拉伯语' };
const SwitchLng = ({ lngs }) => {
const lng = parse(window.location.search).language || Cookie.get('language') || 'en-US';
const handleSwitch = useCallback(l => {
Cookie.set('language', l);
const search = parse(window.location.search) || {};
search.language = l;
const { origin, pathname } = window.location;
window.location.href = `${origin}${pathname}?${stringify(search)}`;
}, []);
return (
{lngs.map(l => (
{i18nMap[l]}
{rtlLngs.includes(l) &&
RTL
}
))}
);
};
class RTLHelper {
constructor(lngs) {
const all = ['en-US', 'ar-EG', 'zh-CN'].concat(lngs || []);
this.supportLngs = [...new Set(all)];
this.renderDOM();
}
renderDOM() {
const div = document.createElement('div');
document.body.appendChild(div);
ReactDOM.render(
, div);
}
}
export default RTLHelper;Conclusion
The article presents a complete workflow for adapting web UI to RTL languages in the Cloud Music overseas project. By combining a direction -based HTML attribute, a lightweight runtime helper, and the postcss-rtlcss plugin (using Mode.combined ), developers can support both LTR and RTL layouts with minimal code changes and without sacrificing third‑party component compatibility.
References
MATERIAL DESIGN – Bidirectionality
CSS direction
CSS logical properties
postcss-rtlcss
NetEase Cloud Music Tech Team
Official account of NetEase Cloud Music Tech Team
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.