PostCSS plugin to build Cascading Style Sheets (CSS) with Left-To-Right (LTR) and Right-To-Left (RTL) rules using RTLCSS
nmp install postcss-rtlcss --save-devyarn add postcss-rtlcss -dconst postcss = require('postcss');
const { postcssRTLCSS } = require('postcss-rtlcss');
const options = { ... available options ... };
const result = postcss([
postcssRTLCSS(options)
]).process(cssInput);
const rtlCSS = result.css;import postcss from 'postcss';
import { postcssRTLCSS } from 'postcss-rtlcss';
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: {
ident: 'postcss',
plugins: () => [ require('postcss-rtlcss')(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 but each direction will have their specific CSS declarations and there is not need to override properties.
.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;
}This is the alternative method, it will generate less code because it lets the main rule intact and generates a shorter specific rule 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: unset;
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;
}But this method has a disadvantage:
Disadvantage of the override method
Use this method carefully. It 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 HTML and CSS codes:
<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.
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: unset;
padding-left: 10px;
}Now the div has a padding of 20px 10px 20px 20px in LTR and 20px 0 20px 10px in RTL, because the override of the class test2 doesn't take into account that this class could be used with test1 having the same properties. The solution, 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, 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;
}All the options are optional, and a default value will be used, if any of them is omitted or de 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 |
| source | Source (string) |
Source.ltr |
The direction from which the final CSS will be generated |
| processUrls | boolean |
false |
Change the strings using the string map also in URLs |
| processKeyFrames | boolean |
false |
Flip keyframe animations |
| useCalc | boolean |
false |
Flips background-position, background-position-x and transform-origin properties if they are expressed in length units using calc |
| stringMap | PluginStringMap (array) |
Check below | An array of strings maps that will be used to make the replacements |
Expand
The mode option has been explained in the Output using the combined mode and Output using the override mode sections. To avoid using magic strings, the package exposes an object with these values, but it is possible to using strings values:
import postcss from 'postcss';
import { postcssRTLCSS, Mode } from 'postcss-trlcss';
const input = '... css code ...';
const optionsCombined = { mode: Mode.combined }; // This is the default value
const optionsOverride = { mode: Mode.override };
const outputCombined = postcss([
postcssRTLCSS(optionsCombined)
]).process(input);
const outputOverride = postcss([
postcssRTLCSS(optionsOverride)
]).process(input);Expand
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;
}Expand
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';
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';
const options = {
mode: Mode.override,
source: Source.rtl
};.test1, .test2 {
left: 10px;
}
[dir="ltr"] .test1, [dir="ltr"] .test2 {
left: unset;
right: 10px;
}Expand
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;
}Expand
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);
}
}Expand
This options, when it is enabled, flips background-position, 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;
}Expand
This options provides an array of strings maps that will be used to make the replacements:
// This is the default string map object
const options = {
stringMap: [
{
search : ['left', 'Left', 'LEFT'],
replace : ['right', 'Right', 'RIGHT']
},
{
search : ['ltr', 'Ltr', 'LTR'],
replace : ['rtl', 'Rtl', 'RTL'],
}
]
};Control directives are placed between rules or declaration. They can target a single node or a set of nodes.
| Directive | Description |
|---|---|
/*rtl:ignore*/ |
Ignores processing of the following rule or declaration |
/*rtl:begin:ignore*/ |
Starts an ignoring block that will ignore any rule or declaration |
/*rtl:end:ignore*/ |
Ends an ignoring block |
Expand
This directive ignores processing of the following rule or declaration. In the next block the whole declaration will be ignored:
/*rtl:ignore*/
.test1, .test2 {
text-align: left;
left: 10px;
}.test1, .test2 {
text-align: left;
left: 10px;
}In the next block only the left property will be ignored:
.test3, .test4 {
text-align: left;
/*rtl:ignore*/
left: 10px;
}.test3, .test4 {
left: 10px;
}
[dir="ltr"] .test3, [dir="ltr"] .test4 {
text-align: left;
}
[dir="rtl"] .test3, [dir="rtl"] .test4 {
text-align: right;
}Expand
These directives should be used together, they will provide the beginning and the end for ignoring rules or declarations.
Note: The directives inserted between these blocks will be ignored and maintained in the final output.
Ignoring multiple rules:
/*rtl:begin:ignore*/
.test1, .test2 {
left: 10px;
text-align: left;
}
.test3 {
padding: 1px 2px 3px 4px;
}
/*rtl:end:ignore*/.test1, .test2 {
left: 10px;
text-align: left;
}
.test3 {
padding: 1px 2px 3px 4px;
}Ignoring multiple declarations:
.test1, .test2 {
left: 10px;
/*rtl:begin:ignore*/
margin-left: 4em;
padding: 1px 2px 3px 4px;
/*rtl:end:ignore*/
text-align: left;
}.test1, .test2 {
margin-left: 4em;
padding: 1px 2px 3px 4px;
}
[dir="ltr"] .test1, [dir="ltr"] .test2 {
left: 10px;
text-align: left;
}
[dir="rtl"] .test1, [dir="rtl"] .test2 {
right: 10px;
text-align: right;
}Value directives are placed any where inside the declaration value. They target the containing declaration node.
| Directive | Description |
|---|---|
/*rtl:ignore*/ |
Ignores processing of the declaration |
/*rtl:append{value}*/ |
Appends {value} to the end of the declaration value |
/*rtl:insert:{value}*/ |
Inserts {value} to where the directive is located inside the declaration value |
/*rtl:prepend:{value}*/ |
Prepends {value} to the begining of the declaration value |
/*rtl:{value}*/ |
Replaces the declaration value with {value} |
Expand
This directive ignores processing of the current declaration:
.test1, .test2 {
text-align: left /*rtl:ignore*/;
left: 10px;
}.test1, .test2 {
text-align: left;
}
[dir="ltr"] .test1, [dir="ltr"] .test2 {
left: 10px;
}
[dir="rtl"] .test1, [dir="rtl"] .test2 {
right: 10px;
}Expand
This directive appends {value} to the end of the declaration value:
.test1, .test2 {
padding: 10px /*rtl:append20px*/;
left: 10px;
}[dir="ltr"] .test1, [dir="ltr"] .test2 {
padding: 10px;
left: 10px;
}
[dir="rtl"] .test1, [dir="rtl"] .test2 {
padding: 10px 20px;
right: 10px;
}Expand
This directive inserts {value} to where the directive is located inside the declaration value:
.test1, .test2 {
padding: 10px/*rtl:insert 20px*/ 5px;
left: 10px;
}[dir="ltr"] .test1, [dir="ltr"] .test2 {
padding: 10px 5px;
left: 10px;
}
[dir="rtl"] .test1, [dir="rtl"] .test2 {
padding: 10px 20px 5px;
right: 10px;
}Expand
This directive prepends {value} to the begining of the declaration value:
.test1, .test2 {
font-family: Arial, Helvetica/*rtl:prepend:"Droid Arabic Kufi", */;
left: 10px;
}[dir="ltr"] .test1, [dir="ltr"] .test2 {
font-family: Arial, Helvetica;
left: 10px;
}
[dir="rtl"] .test1, [dir="rtl"] .test2 {
font-family: "Droid Arabic Kufi", Arial, Helvetica;
right: 10px;
}Expand
This directive replaces the declaration value with {value}:
.test1, .test2 {
font-family: Arial, Helvetica/*rtl:"Droid Arabic Kufi"*/;
left: 10px;
}[dir="ltr"] .test1, [dir="ltr"] .test2 {
font-family: Arial, Helvetica;
left: 10px;
}
[dir="rtl"] .test1, [dir="rtl"] .test2 {
font-family: "Droid Arabic Kufi";
right: 10px;
}If you do not use PostCSS, add it according to official docs and set this plugin in settings.