Manage RTL CSS with Sass and Grunt
As a native Arabic speaker I have worked on multilingual websites before, some already existed with LTR (Left to Right) CSS support which I would then have to add support for RTL (Right to Left) and some projects have started from scratch. Before I started using Sass and Grunt, supporting both directions was a nightmare and a time wasting process with much code repetition.
The important thing when working on multilingual projects using both directions is to write CSS that supports both RTL and LTR in an effective, automated and dynamic way so that we don’t have to repeat or override CSS.
The differences between the two directions in general and in most cases is the float direction, text alignment, padding and margin values.
The Problem
Let’s see how supporting RTL and LTR in the same project could be a bit cumbersome in practice and how we can solve this. In some cases, before adding direction support, we would add a new CSS file in the header and start to override and repeat code over and over to change some CSS properties like floats
, padding-left
or text-align
for different components.
Let’s say this is the code for the RTL language template, we have added the lang="ar"
attribute for the language support, this attribute value would probably be dynamically changed based on the language detection on the server side.
<!DOCTYPE html>
<html lang="ar">
<head>
<link rel="stylesheet" href="css/app.css">
<link rel="stylesheet" href="css/rtl-app.css">
</head>
<body>
<main>Main contant</main>
<aside>Sidebar</aside>
</body>
</html>
The layout requires that in the normal direction (LTR) the main
section should be floated to the left, while aside
should be to the right.
/* app.css */
main { float: left; }
aside { float: right; }
Now for the RTL direction we should do the same thing above but in the opposite direction or to mirror the layout, this will be the code to override the original style in the same file.
/* app.css */
[lang='ar'] main { float: right; }
[lang='ar'] aside { float: left; }
or in a new CSS file appended in the header (as in the example above) we can do it like so.
/* rtl-app.css */
main { float: right; }
aside { float: left; }
The problem here is that we write more code to override the original code, loading more than one CSS file. This is not a good practice and is time consuming.
Now how we can improve this workflow, the solution I have used is to use a CSS preprocessor like Sass and a JavaScript task runner like Grunt to automate and enhance the workflow.
Setup Grunt
By using Grunt and Sass we can automate and solve the problems we have, the theory here is to write our styles in one core file and then generate two other files for each direction then include each file in the header based on the language used.
The Grunt task used for compiling Sass to CSS is grunt-sass.
{
"name": "rtl-sass-grunt",
"version": "0.1.0",
"devDependencies": {
"grunt": "^0.4.5",
"grunt-sass": "^0.16.1"
}
}
module.exports = function(grunt) {
grunt.initConfig({
pkg: grunt.file.readJSON('package.json'),
sass: {
dist: {
files: {
'css/ltr-app.css': 'scss/ltr-app.scss',
'css/rtl-app.css': 'scss/rtl-app.scss'
}
}
}
});
grunt.loadNpmTasks('grunt-sass');
grunt.registerTask('default', ['sass']);
};
The Sass task takes two files one for ltr-app.scss
and rtl-app.scss
then generate the CSS files from them. In both files we will define some variables for floats and directions and then import the core Sass file at the end.
ltr-app.scss
file
// LTR languages directions.
$default-float: left;
$opposite-float: right;
$default-direction: ltr;
$opposite-direction: rtl;
// Import the main style
@import 'style';
And rtl-app.scss
// RTL languages directions.
$default-float: right;
$opposite-float: left;
$default-direction: rtl;
$opposite-direction: ltr;
// Import the main style
@import 'style';
We can include these variables in external files, but this is just for keeping things more clear and to give you the main idea.
The style.scss
will include all our CSS or include other project files. This file will imported in both ltr-app.scss
and rtl-app.scss
.
And this is an example of what style.scss
looks like.
body {
direction: $default-direction;
}
.media {
float: $default-float;
padding-#{$opposite-float}: 10px;
}
.button { background-image: url("images/arror-#{default-float}.png"); }
We have used the Sass variables like $default-float
and Sass interpolation in padding-#{$opposite-float}
, then Grunt can take care of this and generate two files as:
/* ltr-app.css */
body { direction: ltr; }
.media {
float: left;
padding-right: 10px;
}
.button { background-image: url(images/arror-left.png); }
/* rtl-app.css */
body { direction: rtl; }
.media {
float: right;
padding-left: 10px;
}
.button { background-image: url(images/arror-right.png); }
A good trick I experienced before is how to add an image with a specific direction as a CSS background, we can create two images arror-left.png
and arror-right.png
and in the Sass code above the variable will change between left
and right
.
Server Side Setup
For more flexibility when working on multilingual projects, configuring the back-end to provide some variables to use will be important.
What we need from the back-end is to define similar variables like we have done with Sass but this time we will use them inside templates or views. Every back-end solution should be able to provide a way to create theses variables and pass them in views. Variables like def-float
and def-direction
.
Setting up back-end configuration will enable us to switch between the generated CSS files for the detected direction.
<!DOCTYPE html>
<html lang="lang">
<head>
<%= if def-direction is ltr %>
<link rel="stylesheet" href="css/ltr-app.css">
<%= else %>
<link rel="stylesheet" href="css/rtl-app.css">
<%= end %>
</head>
<body>
<main>Main contant</main>
<aside>Sidebar</aside>
</body>
</html>
Or we can use the variable inside the CSS file name itself.
<link rel="stylesheet" href="css/#{def-direction}-app.css">
The if statement above and the file name variable is just a Pseudocode and it will depend on your template engine and your server side configuration.
Working with Helper Classes and Templates
When working with Helper Classes mixed with template files like HTML, erb or any other template engine, we need to dynamically change the class names based on the direction, let’s see an example.
<div class="text-left">
<!-- content -->
</div>
.text-left { text-align: left; }
.text-right { text-align: right; }
The div
content will always align the content to the left as it takes the text-left
class, but we need a way to dynamically change the -left
part to -right
for RTL.
We can solve this issue by using one of our template variables.
<div class="text-#{default-float}">
<!-- content -->
</div>
Sass Mixins
We have used Sass variables and interpolations already, another way for making this simpler is to build another set of Sass mixins.
@mixin float($dir) {
@if $dir == left {
float: $def-float;
} @else if $dir == right {
float: $opp-float;
} @else {
float: $dir;
}
}
@mixin text-align($dir) {
@if $dir == left {
text-align: $def-float;
} @else if $dir == right {
text-align: $opp-float;
} @else {
text-align: $dir;
}
}
@mixin padding-left($unit) {
padding-#{$def-float}: $unit;
}
@mixin padding-right($unit) {
padding-#{$opp-float}: $unit;
}
We can later use these mixins like this
.media {
@include float(left);
@include padding-right(10px);
@include text-align(left);
}
If you’re wanting more mixins for different CSS that is affected by LTR / RTL you can take a look at bi-app-sass.
Conclusion
Here we have discussed how making multilingual sites shouldn’t have to mean we are repeating chucks of code. We learned how we can tackle RTL and LTR languages using Sass. We’ve created a few mixins to demonstrate how we to do this and discussed how we can get the back-end to help make things even easier.
You can use whatever tool other than Grunt and Sass for doing the same thing, getting the idea of using Sass variables for manipulating directions and changing variables in template files is the core idea. I’ve created a Github repo for this article. You can check out the code used in rtl-grunt-sass.