Getting started with Less
Less, as defined by Wikipedia:
Less (also known as LESS) is a dynamic stylesheet language that can be compiled into Cascading Style Sheets (CSS), or can run on the client-side and server-side.
It's perfectly named, because it results in you spending a lot "less" effort maintaining and extending our CSS as it keeps growing.
One doesn't have to learn a whole new set of syntax, as it's exactly the same as CSS with a few added bits in order to manage the new capabilities. One can take any given CSS stylesheet, and compile it with less, which makes it easier to when converting our existing stylesheets. The main new functionalities that Less provides are:
- variables
- functions with parameters
- methematical operations
Also, Less makes it easy for us to to "compile" our CSS into a minified form that will result in:
- removing all spacing and comments
- This makes it harder for others to use our work by taking it from the site.
- faster page loads since less bandwidth is needed.
Installing On Ubuntu
Firstly, I recommend installing NodeJS from the maintainers rather than from the ubuntu packages.
Now install install Less with either of the following commands:
# Install through ubuntu packages
sudo apt-get install node-less -y
# Later version through npm
sudo npm install -g less
To compile a less file into CSS, simply run:
lessc -x [filename].less > [filename].css
-x
to stop the minification process if we desire.
Variables And Maths
Below is an example less file that demonstrates how to use variables and maths to create better font sizes with ease.
@baseFontSize: 1em;
@fontSizeStepping: 1.5;
body {
font-size: @baseFontSize;
}
h3 {
font-size: @baseFontSize * @fontSizeStepping;
}
h2 {
font-size: @baseFontSize * @fontSizeStepping * @fontSizeStepping;
}
h1 {
font-size: @baseFontSize * @fontSizeStepping * @fontSizeStepping * @fontSizeStepping;
}
We can see that variables are declared with the @
symbol and we could say that they have a "global" scope.
One might think that using @fontSizeStepping * @fontSizeStepping * @fontSizeStepping
is extremely wasteful,
but don't worry. our less file will compile in absolutely no time at all, and only the end result of the operation will be placed in the CSS file as decimal.
Warning: Only the last value of a variable in the entire file will be used when generating the CSS. To demonstrate this, compile the following code snippet and look at what font-size the body is set to.
@baseFontSize: 1em;
@fontSizeStepping: 1.5;
body {
font-size: @baseFontSize;
}
h3 {
font-size: @baseFontSize * @fontSizeStepping;
}
h2 {
font-size: @baseFontSize * @fontSizeStepping * @fontSizeStepping;
}
h1 {
font-size: @baseFontSize * @fontSizeStepping * @fontSizeStepping * @fontSizeStepping;
}
@baseFontSize: 2em;
We can see that the body has a font-size of 2em
, not 1em
like one might expect. This makes the first declaration of @baseFontSize
worse than redundant because it will likely confuse others later. However, it does mean that we can initalize variables anywhere in the file, even after they are being used. I recommend not doing this unless there is a good reason.
"Functions" and Parameters
Functions are refferred to as "mixins". They appear exactly the same as a class, except that they have '()' as shown below.
.myClass {
font-size: 12px;
}
.myMixin() {
font-size: 12px;
}
The previous code will wimply output the following CSS file:
.myClass {
font-size: 12px;
}
How Does This Help?
This is best demonstrated with an example. You may have discovered the new box-shadow feature in CSS3, but getting it to work in every browser can be a real pain. Not only that, but 99% of the time, we want to create same effect, but may want to change the size of the shadow. This can be done with the following code sample:
.addBoxShadow(@size) {
@color: black;
moz-box-shadow: 0 0 @size @color;
-webkit-box-shadow: 0 0 @size @color;
box-shadow: 0 0 @size @color;
-ms-filter: "progid:DXImageTransform.Microsoft.Shadow(Strength=@size, Direction=0, Color=@color)";
filter: progid:DXImageTransform.Microsoft.Shadow(Strength=@size, Direction=0, Color=@color);
box-shadow: 0 0 @size 0 @color;
}
.myBox {
background-color: grey;
.addBoxShadow(5px);
}
Now, whenever we want to add a shadow to a div, all we have to do is call addBoxShadow
and pass it the size.
This makes life much easier, and makes our CSS neater as we add box shadows to multiple classes.
Mixins And Classes are Interchangeable
So far, we have only used mixins to define properties/attributes. Mixins can also define classes, and can be used interchangeably with classes. For example, the following example is perfectly valid.
.generalProperties()
{
font-size: 2em;
color: black
}
.defineHeaders() {
h1,h2,h3,h4 {
.generalProperties()
}
}
.defineHeaders();
It's not very useful (who wants all their headers the same size?), but it does demonstrate a mixin initializing the various headers within itself, and calling another mixin in order to set their properties.
Variable's Scope and "Returns"
You may have noticed in our previous example, that we defined @color: black
inside the mixin so that we could easily change of all our site's box-shadows at a later date with a one-line change. One may be concerned from the earlier warning, where we learned that variables have a global scope and that the last defined variable is used throughout the file. We might have a previous definition of @color
that we don't want to be overridden as it's supposed to be applied to the whole document as shown below:
// Set the documents global color
@color: black;
.setBgColor() {
@color: white;
background-color: @color;
}
.myBox {
.setBgColor();
}
.myTextbox {
color: @color;
}
Does this mean that myTextbox will now output text in the white color instead of black!? Well if we compile it, we get the following output:
.myBox {
background-color: #ffffff;
}
.myTextbox {
color: #000000;
}
As you can see, our boxes still have a white background color (#ffffff), and our text is still black (#000000). This is because variables will be overridden with specificity of scope. Thus, if we define a variable inside a mixin, that will be the variable's value inside the mixin, no matter what, or where, the variable was set to outside of the mixin.
Mixins cannot "return" values such as in most other languages. However, variables defined inside the mixin will also carry to the outside scope as well, except that it will not override a variable that was initialized in that scope due to the spcificity rule just described. The usefulness of this is best shown in the example below:
// --- Mixins ---
.setDarkColorScheme() {
@color: white;
@bgColor: black;
}
.setLightColorScheme() {
@color: black;
@bgColor: white;
}
// --- Classes ---
.myDarkBox {
.setDarkColorScheme();
color: @color;
background-color: @bgColor;
}
.lightTextBox {
.setLightColorScheme();
color: @color;
background-color: @bgColor;
}
This compiles to:
.myDarkBox {
color: #ffffff;
background-color: #000000;
}
.lightTextBox {
color: #000000;
background-color: #ffffff;
}
As you can see, now we can use mixins to setup color schemes for our classes. The main advantage of this is not having to use a massive list of variables with long-winded names at the top of our files:
@lightTextBoxBgColor: white;
@lightTextBoxColor: black;
@darkTextBoxBgColor: grey;
@lightTextBoxColor: white;
.myDarkBox {
color: @lightTextBoxColor;
background-color: @darkTextBoxBgColor;
}
.lightTextBox {
color: @lightTextBoxColor;
background-color: @lightTextBoxBgColor;
}
.anotherTextBox {
color: @lightTextBoxColor;
background-color: @lightTextBoxBgColor;
}
This example looks simpler and "prettier" with far less lines of code, but imagine what it would look like, and how maintainable it would be, if we we hand a couple dozen classes, and a few more color schemes? By using a mixin, we also prevent developers mixing and matching the colors across different colour schemes, which would end up causing a nightmare when we later tried to adjust a specific color scheme. Instead, they should alter the colour scheme mixin, or create a new one.
Warning Don't forget about our previous warning about compilation and global scope. Can you spot the issue with the following code sample?
.setDarkColorScheme() {
@color: white;
@bgColor: black;
}
.setLightColorScheme() {
@color: black;
@bgColor: white;
}
// ----------------------------------
// --- Dark Color Scheme Classes ---
// ----------------------------------
.setDarkColorScheme();
.myDarkBox {
color: @color;
background-color: @bgColor;
}
.darkElement1 {
color: @color;
background-color: @bgColor;
}
// ----------------------------------
// --- Light Color Scheme Classes ---
// ----------------------------------
.setLightColorScheme();
lightTextBox {
color: @color;
background-color: @bgColor;
}
lightElement2 {
color: @color;
background-color: @bgColor;
}
Answer
If we compile that Less file, all the elements will have the last invoked color scheme
.myDarkBox {
color: #ffffff;
background-color: #000000;
}
.darkElement1 {
color: #ffffff;
background-color: #000000;
}
lightTextBox {
color: #ffffff;
background-color: #000000;
}
lightElement2 {
color: #ffffff;
background-color: #000000;
}
Overloading
Less provides what I call an "extended" version of overloading because it's very similar to overloading in Java, but there is a subtle addition. This is best demonstrated with an example:
// -----------Set Colors --------------
.createBox() {
color: black;
background-color: white;
}
.createBox(@fgColor) {
color: @fgColor;
background-color: white;
}
// ------------ Set Widths -----------
.createBox() {
width: 200px;
height: 200px;
}
.createBox(@color) {
width: 200px;
height: 200px;
}
// ------------ Classes -------------
.box1 {
.createBox();
}
.boxWithYellowFont {
.createBox(yellow);
}
As you can see, the createBox
mixin has four definitions, two of which "clash" with the other two as they share the samee signature. In Java, this would result in an error, but in Less, this is perfectly acceptable. When two mixins share the same signature, all of those mixins are invoked when they match the called signature.
Thus, the previous code sample produces the following output:
.box1 {
color: black;
background-color: white;
width: 200px;
height: 200px;
}
.boxWithYellowFont {
color: #ffff00;
background-color: white;
width: 200px;
height: 200px;
}
This example shows that overloading can be useful, but can also lead to mess and confusion. For example, it looks rather odd that there are two width definitions for createBox
which do exactly the same thing, except one has a @color
parameter which never gets used. Someone else who is reading the codebase for the first time might see this and remove the definition, thinking that it's redundant. This is why I advise that you only overload with one of the following rules per mixin, never both:
- every definitions must have the same signature.
- each definition must have a unique signature (Java style).
Nesting
Less provides us an easy way to define rules for elements that are within other elements.
I am now going to create a less file that defines the h1,h2,h3,p, and li elements for the various sections of a site. We want to use a smaller font in the navigation areas found in the sidebar, header and footer compared to the content section of the site. However, we want to use very large font in the title that is found within the header.
// Define our element set for a section of the site
.defineElements(@color, @baseFontSize) {
p,
li,
h1,
h2,
h3 {
color: @color;
}
h1 {
font-size: @baseFontSize * 3;
}
h2 {
font-size: @baseFontSize * 2;
}
h3 {
font-size: @baseFontSize * 1;
}
}
// ------ End Of Mixins --------------
.defineElements(grey, 1em);
.sidebar,
.header{
.defineElements(black, 0.9em);
}
.header {
.title {
.defineElements(grey, 1.5em);
}
}
.content {
.defineElements(black, 1em);
}
.footer {
.defineElements(grey, 0.8em);
}
Reading the code above, you can clearly see the relationship the section has on the size of fonts in the site. You would not be able to see this in the final CSS, and writing the CSS manually to do this would be quite difficult and long-winded.
It's worth noting that this bit from the code above:
.header {
.title {
.defineElements(grey, 0.9em);
}
}
Could be rewritten as
.header .title {
.defineElements(grey, 0.9em);
}
However, I find that the first example is clearer to read and understand.
Conclusion
Less gives us the power to start defining the relationship between our classes and simplify our codebase, but with more power comes more responsibility. Less will not be able to prevent us from creating a confusing mess. Great care must be taken to ensure that the capabilities that Less provides us are not overused and abused. Otherwise, we will end up back where we started.
We have only covered the most useful basics. There is still quite a bit more that we haven't covered, such as namespaces, guards, keywords,
the use of @import
, and the use of the &
symbol. We will discuss those next time.
First published: 16th August 2018