PostCSS plugin to build Cascading Style Sheets (CSS) with Left-To-Right (LTR) and Right-To-Left (RTL) rules using RTLCSS. RTLCSS allows one to flip an entire CSS file with the intention of using the original CSS for one direction and the new generated one for the other. What PostCSS RTLCSS does, is to create a single CSS file with both directions or to create a minimal CSS file only with the flipped rules with the intention of overriding the main one.
https://elchininet.github.io/postcss-rtlcss/
npm install postcss-rtlcss --save-devpnpm add -D postcss-rtlcssyarn add postcss-rtlcss -Dconst postcss = require('postcss');
const postcssRTLCSS = require('postcss-rtlcss');
const { Mode, Source } = require('postcss-rtlcss/options');
const options = { ... available options ... };
const result = postcss([
postcssRTLCSS(options)
]).process(cssInput);
const rtlCSS = result.css;import postcss from 'postcss';
import postcssRTLCSS from 'postcss-rtlcss';
import { Mode, Source } from 'postcss-rtlcss/options';
const options = { ... available options ... };
const result = postcss([
postcssRTLCSS(options)
]).process(cssInput);
const rtlCSS = result.css;rules: [
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' },
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
postcssRTLCSS(options)
]
}
}
}
]
}
].test1, .test2 {
background-color: #FFF;
background-position: 10px 20px;
border-radius: 0 2px 0 8px;
color: #666;
padding-right: 20px;
text-align: left;
transform: translate(-50%, 50%);
width: 100%;
}
.test3 {
direction: ltr;
margin: 1px 2px 3px;
padding: 10px 20px;
text-align: center;
}This is the recommended method, it will generate more CSS code because each direction will have their specific prefixed rules but it is the safest option.
.test1, .test2 {
background-color: #FFF;
background-position: 10px 20px;
color: #666;
width: 100%;
}
[dir="ltr"] .test1, [dir="ltr"] .test2 {
border-radius: 0 2px 0 8px;
padding-right: 20px;
text-align: left;
transform: translate(-50%, 50%);
}
[dir="rtl"] .test1, [dir="rtl"] .test2 {
border-radius: 2px 0 8px 0;
padding-left: 20px;
text-align: right;
transform: translate(50%, 50%);
}
.test3 {
margin: 1px 2px 3px;
padding: 10px 20px;
text-align: center;
}
[dir="ltr"] .test3 {
direction: ltr;
}
[dir="rtl"] .test3 {
direction: rtl;
}Important
This method is not recommended, check below why
This is one of the alternative methods to override. It will generate less code because it lets the main rule intact most of the time and generates shorter specific rules to override the properties that are affected by the direction of the text.
.test1, .test2 {
background-color: #FFF;
background-position: 10px 20px;
border-radius: 0 2px 0 8px;
color: #666;
padding-right: 20px;
text-align: left;
transform: translate(-50%, 50%);
width: 100%;
}
[dir="rtl"] .test1, [dir="rtl"] .test2 {
border-radius: 2px 0 8px 0;
padding-right: 0;
padding-left: 20px;
text-align: right;
transform: translate(50%, 50%);
}
.test3 {
direction: ltr;
margin: 1px 2px 3px;
padding: 10px 20px;
text-align: center;
}
[dir="rtl"] .test3 {
direction: rtl;
}Important
This method is not recommended, check below why
This is the second alternative method to override. It generates the minimum amount of code because it only outputs the rules that have been flipped and without prefixing them. The intention of this method is to generate a separate stylesheet file that will be loaded on top of the original one to override those rules that need to be flipped in certain direction.
.test1, .test2 {
border-radius: 2px 0 8px 0;
padding-right: 0;
padding-left: 20px;
text-align: right;
transform: translate(50%, 50%);
}
.test3 {
direction: rtl;
}- Some directives as
/*rtl:freeze*/,/*rtl:begin:freeze*/and/*rtl:end:freeze*/do not work with these methods - They can override a property that is coming from another class if multiple classes are used at the same time. Take a look at the next
HTMLandCSScodes:
<div class="test1 test2">
This is an example
</div> .test1 {
background: #666;
color: #FFF;
padding: 20px;
}
.test2 {
padding-right: 10px;
}Using the combined method, the generated code will be the next one:
.test1 {
background: #666;
color: #FFF;
padding: 20px;
}
[dir="ltr"] .test2 {
padding-right: 10px;
}
[dir="rtl"] .test2 {
padding-left: 10px;
}So, the div will have a padding of 20px 10px 20px 20px in LTR and 20px 20px 20px 10px in RTL. Everything will work as expected here.
However, using the override method the generated code will be the next one:
.test1 {
background: #666;
color: #FFF;
padding: 20px;
}
.test2 {
padding-right: 10px;
}
[dir="rtl"] .test2 {
padding-right: 0;
padding-left: 10px;
}And using the diff method the generated code will be the next one:
.test2 {
padding-right: 0;
padding-left: 10px;
}Now the div has a padding of 20px 10px 20px 20px in LTR and 20px 0 20px 10px in RTL, because when the class test2 is overriden, it is not taken into account that it could be used with test1 having the same properties. The workaround, in this case, is to provide the property that has been inherited:
.test1 {
background: #666;
color: #FFF;
padding: 20px;
}
.test2 {
padding-left: 20px;
padding-right: 10px;
}So, using the override method the generated code will be:
.test1 {
background: #666;
color: #FFF;
padding: 20px;
}
.test2 {
padding-left: 20px;
padding-right: 10px;
}
[dir="rtl"] .test2 {
padding-right: 20px;
padding-left: 10px;
}And using the diff method the generated code will be:
.test2 {
padding-right: 20px;
padding-left: 10px;
}All the options are optional, and a default value will be used if any of them is omitted or the type or format of them is wrong
| Option | Type | Default | Description |
|---|---|---|---|
| mode | Mode (string) |
Mode.combined |
Mode of generating the final CSS rules |
| ltrPrefix | string or string[] |
[dir="ltr"] |
Prefix to use in the left-to-right CSS rules |
| rtlPrefix | string or string[] |
[dir="rtl"] |
Prefix to use in the right-to-left CSS rules |
| bothPrefix | string or string[] |
[dir] |
Prefix to create a new rule that affects both directions when the specificity of the ltr or rtl rules will override its declarations |
| prefixSelectorTransformer | function |
null |
Transform function to have more control over the selectors prefixing logic |
| safeBothPrefix | boolean |
false |
Add the bothPrefix to those declarations that can be affected by the direction to avoid them being overridden by specificity |
| ignorePrefixedRules | boolean |
true |
Ignores rules that have been prefixed with some of the prefixes contained in ltrPrefix, rtlPrefix, or bothPrefix |
| source | Source (string) |
Source.ltr |
The direction from which the final CSS will be generated |
| processUrls | boolean |
false |
Change the strings in URLs using the string map |
| processRuleNames | boolean |
false |
Swap two rules containing no directional properties if they match any entry in stringMap when the direction changes |
| processKeyFrames | boolean |
false |
Flip keyframe animations |
| processEnv | boolean |
true |
When processEnv is false, it prevents flipping agent-defined environment variables (safe-area-inset-left and safe-area-inset-right) |
| useCalc | boolean |
false |
Flips background-position-x and transform-origin properties if they are expressed in length units using calc |
| stringMap | PluginStringMap[] |
Check below | An array of strings maps that will be used to make the replacements of the declarations' URLs and to match the names of the rules if processRuleNames is true |
| greedy | boolean |
false |
When greedy is true, the matches of stringMap will not take into account word boundaries |
| aliases | Record<string, string> |
{} |
A strings map to treat some declarations as others |
| processDeclarationPlugins | DeclarationPlugin[] |
[] |
Plugins applied when processing CSS declarations |
| runOnExit | boolean |
false |
Defines which visitor will be used to execute the plugin. If it is false (default value), Once will be used, but if it is true, OnceExit will be used instead. |
The mode option has been explained in the Output using the combined mode, the Output using the override mode, and the Output using the diff mode sections. To avoid using magic strings, the package exposes an object with these values, but it is possible to use strings values anyway:
import postcss from 'postcss';
import postcssRTLCSS from 'postcss-rtlcss';
import { Mode } from 'postcss-rtlcss/options';
const input = '... css code ...';
const optionsCombined = { mode: Mode.combined }; // This is the default value
const optionsOverride = { mode: Mode.override };
const optionsDiff = { mode: Mode.diff };
const outputCombined = postcss([
postcssRTLCSS(optionsCombined)
]).process(input);
const outputOverride = postcss([
postcssRTLCSS(optionsOverride)
]).process(input);
const outputDiff = postcss([
postcssRTLCSS(optionsDiff)
]).process(input);These two options manage the prefix strings for each direction. They can be strings or arrays of strings:
.test1, .test2 {
left: 10px;
}
.test3,
.test4 {
text-align: left;
}const options = {
ltrPrefix: '.ltr',
rtlPrefix: '.rtl'
};.ltr .test1, .ltr .test2 {
left: 10px;
}
.rtl .test1, .rtl .test2 {
right: 10px;
}
.ltr .test3,
.ltr .test4 {
text-align: left;
}
.rtl .test3,
.rtl .test4 {
text-align: right;
}const options = {
ltrPrefix: ['[dir="ltr"]', '.ltr'],
rtlPrefix: ['[dir="rtl"]', '.rtl']
};[dir="ltr"] .test1, .ltr .test1, [dir="ltr"] .test2, .ltr .test2 {
left: 10px;
}
[dir="rtl"] .test1, .rtl .test1, [dir="rtl"] .test2, .rtl .test2 {
right: 10px;
}
[dir="ltr"] .test3,
.ltr .test3,
[dir="ltr"] .test4,
.ltr .test4 {
text-align: left;
}
[dir="rtl"] .test3,
.rtl .test3,
[dir="rtl"] .test4,
.rtl .test4 {
text-align: right;
}This prefix will be used in some specific cases in which a ltr or rtl rule will override declarations located in the main rule due to specificity. Consider the next example using the option processUrls as true:
.test1 {
background: url('icons/ltr/arrow.png');
background-size: 10px 20px;
width: 10px;
}The generated CSS would be:
.test1 {
background-size: 10px 20px;
width: 10px;
}
[dir="ltr"] .test1 {
background: url('icons/ltr/arrow.png');
}
[dir="rtl"] .test1 {
background: url('icons/rtl/arrow.png');
}In the previous case, the background-size property has been overridden by the background one. Even if we change the order of the rules, the last ones have a higher specificity, so they will rule over the first one.
To solve this, another rule will be created at the end using the bothPrefix parameter:
.test1 {
width: 10px;
}
[dir="ltr"] .test1 {
background: url('icons/ltr/arrow.png');
}
[dir="rtl"] .test1 {
background: url('icons/rtl/arrow.png');
}
[dir] {
background-size: 10px 20px;
}And no matter the direction, the background-size property is respected.
This function will be used to transform the selectors and prefixing them at our will. The first parameter will be the prefix that will be used and the second the current selector:
Note
- If the function doesn‘t return a string, the default prefixing logic will be used.
- If this function is used, be aware that rules using
html,:rootor::view-transitionwill follow the custom prefixing logic. You should cover these cases.
.test1 {
left: 10px;
padding-right: 5px;
padding-inline-end: 20px;
}If the prefixSelectorTransformer is not sent (default):
[dir="ltr"] .test1 {
left: 10px;
padding-right: 5px;
}
[dir="rtl"] .test1 {
right: 10px;
padding-left: 5px;
}
[dir] .test1 {
padding-inline-end: 20px;
}Setting a prefixSelectorTransformer function
const options = {
prefixSelectorTransformer: function (prefix, selector) {
if (prefix === '[dir]') {
return `.container > ${prefix} > ${selector}`;
}
return `${selector}${prefix}`;
}
};.test1[dir="ltr"] {
left: 10px;
padding-right: 5px;
}
.test1[dir="rtl"] {
right: 10px;
padding-left: 5px;
}
.container > [dir] > .test1 {
padding-inline-end: 20px;
}This option will add the boxPrefix option to those declarations that can be flipped, no matter if they are not overridden in the same rule. This avoids them being overridden by specificity of other flipped declarations contained in other rules. For example, let's consider that we have a div element with the next rules:
<div class="test1 test2">
This is an example
</div> .test1 {
color: #FFF;
padding: 4px 10px 4px 20px;
width: 100%;
}
.test2 {
padding: 0;
}The expecting result is that the padding of the element becomes 0 as it has been reset by test2. With safeBothPrefix in false, the generated CSS will be:
.test1 {
color: #FFF;
width: 100%;
}
[dir="ltr"] .test1 {
padding: 4px 10px 4px 20px;
}
[dir="rtl"] .test1 {
padding: 4px 20px 4px 10px;
}
.test2 {
padding: 0;
}The result is that the padding properties of test1 have more specificity than the same property in tes2, so it is not reset if both rules are applied at the same time. Let's check the result if safeBothPrefix is true:
.test1 {
color: #FFF;
width: 100%;
}
[dir="ltr"] .test1 {
padding: 4px 10px 4px 20px;
}
[dir="rtl"] .test1 {
padding: 4px 20px 4px 10px;
}
[dir] .test2 {
padding: 0;
}As test2 has the same level of specificity as test1, now the result is that the padding is reset if both rules are used at the same time.
This option is to ignore the rules that have been prefixed with one of the prefixes contained in ltrPrefix, rtlPrefix, or bothPrefix:
[dir="ltr"] test {
left: 10px;
}
[dir="rtl"] test {
right: 10px;
}const options = { ignorePrefixedRules: true }; // This is the default value[dir="ltr"] test {
left: 10px;
}
[dir="rtl"] test {
right: 10px;
}const options = { ignorePrefixedRules: false };[dir="ltr"] [dir="ltr"] test {
left: 10px;
}
[dir="rtl"] [dir="ltr"] test {
right: 10px;
}
[dir="ltr"] [dir="rtl"] test {
right: 10px;
}
[dir="rtl"] [dir="rtl"] test {
left: 10px;
}This option manages if the conversion will be from LTR to RTL or vice versa.
.test1, .test2 {
left: 10px;
}import { Mode, Source } from 'postcss-rtlcss/options';
const options = {
mode: Mode.combined,
source: Source.ltr // This is the default value
};[dir="ltr"] .test1, [dir="ltr"] .test2 {
left: 10px;
}
[dir="rtl"] .test1, [dir="rtl"] .test2 {
right: 10px;
}import { Mode, Source } from 'postcss-rtlcss/options';
const options = {
mode: Mode.override,
source: Source.rtl
};.test1, .test2 {
left: 10px;
}
[dir="ltr"] .test1, [dir="ltr"] .test2 {
left: auto;
right: 10px;
}This options manages if the strings of the URLs should be flipped taken into account the string map:
.test1, .test2 {
background-image: url("./folder/subfolder/icons/ltr/chevron-left.png");
left: 10px;
}const options = { processUrls: false }; // This is the default value.test1, .test2 {
background-image: url("./folder/subfolder/icons/ltr/chevron-left.png");
}
[dir="ltr"] .test1, [dir="ltr"] .test2 {
left: 10px;
}
[dir="rtl"] .test1, [dir="rtl"] .test2 {
right: 10px;
}const options = { processUrls: true };[dir="ltr"] .test1, [dir="ltr"] .test2 {
background-image: url("./folder/subfolder/icons/ltr/chevron-left.png");
left: 10px;
}
[dir="rtl"] .test1, [dir="rtl"] .test2 {
background-image: url("./folder/subfolder/icons/rtl/chevron-right.png");
right: 10px;
}If it is true, it swaps two rules containing no directional properties if they match any entry in stringMap when the direction changes
Important
This option will not prefix those rules that have been processed already because they had directional properties.
.test1-ltr {
color: #FFF;
}
.test2-left::before {
content: "\f007";
}
.test2-right::before {
content: "\f010";
}const options = {
processRuleNames: true
};/* This selector will not be processed because it doesn't have a counterpart */
.test1-ltr {
color: #FFF;
}
[dir="ltr"] .test2-left::before {
content: "\f007";
}
[dir="rtl"] .test2-left::before {
content: "\f010";
}
[dir="ltr"] .test2-right::before {
content: "\f010";
}
[dir="rtl"] .test2-right::before {
content: "\f007";
}This option manages if the @keyframes animation rules should be flipped:
.test1 {
animation: 5s flip 1s ease-in-out;
color: #FFF;
}
@keyframes flip {
from {
transform: translateX(100px);
}
to {
transform: translateX(0);
}
}const options = { processKeyFrames: false }; // This is the default value.test1 {
animation: 5s flip 1s ease-in-out;
color: #FFF;
}
@keyframes flip {
from {
transform: translateX(100px);
}
to {
transform: translateX(0);
}
}const options = { processKeyFrames: true };.test1 {
color: #FFF;
}
[dir="ltr"] .test1 {
animation: 5s flip-ltr 1s ease-in-out;
}
[dir="rtl"] .test1 {
animation: 5s flip-rtl 1s ease-in-out;
}
@keyframes flip-ltr {
from {
transform: translateX(100px);
}
to {
transform: translateX(0);
}
}
@keyframes flip-rtl {
from {
transform: translateX(-100px);
}
to {
transform: translateX(0);
}
}This options manages if the agent-defined environment variables should be flipped:
body {
padding:
env(safe-area-inset-top, 10px)
env(safe-area-inset-right, 20px)
env(safe-area-inset-bottom, 30px)
env(safe-area-inset-left, 40px)
;
}
.test1 {
margin-right: env(safe-area-inset-right, 10px);
margin-left: env(safe-area-inset-left, 20px);
}const options = { processEnv: true }; // This is the default value[dir=\\"ltr\\"] body {
padding:
env(safe-area-inset-top, 10px)
env(safe-area-inset-right, 20px)
env(safe-area-inset-bottom, 30px)
env(safe-area-inset-left, 40px)
;
}
[dir=\\"rtl\\"] body {
padding:
env(safe-area-inset-top, 10px)
env(safe-area-inset-right, 40px)
env(safe-area-inset-bottom, 30px)
env(safe-area-inset-left, 20px);
}
[dir=\\"ltr\\"] .test1 {
margin-right: env(safe-area-inset-right, 10px);
margin-left: env(safe-area-inset-left, 20px);
}
[dir=\\"rtl\\"] .test1 {
margin-left: env(safe-area-inset-left, 10px);
margin-right: env(safe-area-inset-right, 20px);
}const options = { processEnv: false };[dir=\\"ltr\\"] body {
padding:
env(safe-area-inset-top, 10px)
env(safe-area-inset-right, 20px)
env(safe-area-inset-bottom, 30px)
env(safe-area-inset-left, 40px)
;
}
[dir=\\"rtl\\"] body {
padding:
env(safe-area-inset-top, 10px)
env(safe-area-inset-left, 40px)
env(safe-area-inset-bottom, 30px)
env(safe-area-inset-right, 20px);
}
[dir=\\"ltr\\"] .test1 {
margin-right: env(safe-area-inset-right, 10px);
margin-left: env(safe-area-inset-left, 20px);
}
[dir=\\"rtl\\"] .test1 {
margin-left: env(safe-area-inset-right, 10px);
margin-right: env(safe-area-inset-left, 20px);
}When this option is enabled, it flips background-position-x and transform-origin properties if they are expressed in length units using calc:
.test {
background-image: url("./folder/subfolder/icons/ltr/chevron-left.png");
background-position-x: 5px;
left: 10px;
transform-origin: 10px 20px;
transform: scale(0.5, 0.5);
}const options = { useCalc: false }; // This is the default value.test {
background-image: url("./folder/subfolder/icons/ltr/chevron-left.png");
background-position-x: 5px;
transform-origin: 10px 20px;
transform: scale(0.5, 0.5);
}
[dir="ltr"] .test {
left: 10px;
}
[dir="rtl"] .test {
right: 10px;
}const options = { useCalc: true };.test {
background-image: url("./folder/subfolder/icons/ltr/chevron-left.png");
transform: scale(0.5, 0.5);
}
[dir="ltr"] .test {
background-position-x: 5px;
left: 10px;
transform-origin: 10px 20px;
}
[dir="rtl"] .test {
background-position-x: calc(100% - 5px);
right: 10px;
transform-origin: calc(100% - 10px) 20px;
}An array of strings maps that will be used to make the replacements of the declarations' URLs and to match rules selectors names if the processRuleNames option is true. The name parameter is optional, but if you want to override any of the default string maps, just add your own using the same name.
// This is the default string map object
const options = {
stringMap: [
{
name: 'left-right',
search : ['left', 'Left', 'LEFT'],
replace : ['right', 'Right', 'RIGHT']
},
{
name: 'ltr-rtl',
search : ['ltr', 'Ltr', 'LTR'],
replace : ['rtl', 'Rtl', 'RTL'],
}
]
};When greedy is true, the matches of the stringMap will not take into account word boundaries.
.test1 {
background: url("icon-left.png");
}
.test2 {
background: url("icon-ultra.png");
}const options = {
processUrls: true,
greedy: false // This is the default value
};[dir="ltr"] .test1 {
background: url("icon-left.png");
}
[dir="rtl"] .test1 {
background: url("icon-right.png");
}
.test2 {
background: url("icon-ultra.png");
}const options = {
processUrls: true,
greedy: true
};[dir="ltr"] .test1 {
background: url("icon-left.png");
}
[dir="rtl"] .test1 {
background: url("icon-right.png");
}
[dir="ltr"] .test2 {
background: url("icon-ultra.png");
}
[dir="rtl"] .test2 {
background: url("icon-urtla.png");
}This property consists of a string map to treat some declarations as others, very useful to flip the values of CSS variables.
:root {
--my-padding: 1rem 1rem 1.5rem 1.5rem;
}
.test {
padding: var(--my-padding);
}:root {
--my-padding: 1rem 1rem 1.5rem 1.5rem;
}
.test {
padding: var(--my-padding);
}const options = {
aliases: {
'--my-padding': 'padding'
}
};[dir="ltr"]:root {
--my-padding: 1rem 1rem 1.5rem 1.5rem;
}
[dir="rtl"]:root {
--my-padding: 1rem 1.5rem 1.5rem 1rem;
}
.test {
padding: var(--my-padding);
}The intention of the processDeclarationPlugins option is to process the declarations to extend or override RTLCSS functionality. For example, we can avoid automatically flipping of background-potion.
.test {
background-position: 0 100%;
}.test {
background-position: 100% 100%;
}const options = {
processDeclarationPlugins: [
{
name: 'avoid-flipping-background',
priority: 99, // above the core RTLCSS plugin which has a priority value of 100
processors: [{
expr: /(background|object)(-position(-x)?|-image)?$/i,
action: (prop, value) => ({prop, value})}
]
}
]
};.test {
background-position: 0 100%;
}This option defines which PostCSS visitor will be used to execute the plugin. By default it is false, so the Once visitor will be used. If it is true, OnceExit will be used instead. Setting this option in true is useful if the plugin is used together with postcss-preset-env because in those cases the plugin will be executed when postcss-preset-env finished all the CSS processing.
For example, let's assume that for the next example, PostCSS RTLCSS is executed together with postcss-preset-env.
.test {
color: red;
border-inline-start-width: thick;
margin-inline-end: 5px;
padding-inline-start: 20px;
}const options = {
runOnExit: false // This is the default value
};.test {
color: red;
border-inline-start-width: thick;
margin-inline-end: 5px;
padding-inline-start: 20px;
}PostCSS RTLCSS didn't apply any change because it was executed at the beginning and it doesn't support CSS logical properties.
const options = {
runOnExit: true
};.test {
color: red;
}
[dir="ltr"] .test {
border-left-width: thick;
margin-right: 5px;
padding-left: 20px;
}
[dir="rtl"] .test {
border-right-width: thick;
margin-left: 5px;
padding-right: 20px;
}postcss-preset-env executed postcss-logical behind the scenes in the CSS and converted its properties from logical to physical. After it finishes, PostCSS RTLCSS is executed and it makes the necessary changes to add the LTR and RTL prefixes.