LESS vs. SASS: Variable semantics

Published by marco on

Updated by marco on

I’ve been using CSS since pretty much its inception. It’s powerful but quite low-level and lacks support for DRY. So, I switched to generating CSS with LESS a while back. This has gone quite well and I’ve been pretty happy with it.

Recently, I was converting some older, theme stylesheets for earthli. A theme stylesheet provides no structural CSS, mostly setting text, background and border colors to let users choose the basic color set. This is a perfect candidate for LESS.

So I constructed a common stylesheet that referenced LESS variables that I would define in the theme stylesheet. Very basically, it looks like this:

crimson.less

@body_color: #800;
@import "theme-base";

theme-base.less

body
{
  background-color: @body_color;
}

This is just about the most basic use of LESS that even an amateur user could possibly imagine. I’m keeping it simple because I’d like to illustrate a subtlety to variables in LESS that tripped me up at first—but for which I’m very thankful. I’ll give you a hint: LESS treats variables as a stylesheet would, whereas SASS treats them as one would expect in a programming language.

Let’s expand the theme-base.less file with some more default definitions. I’m going to define some other variables in terms of the body color so that themes don’t have to explicitly set all values. Instead, a theme can set a base value and let the base stylesheet calculate derived values. If a calculated value isn’t OK for a theme, the theme can set that value explicitly to override.

Let’s see an example before we continue.

theme-base.less

@title_color: darken(@body_color, 25%);
@border_color: @title_color;

body
{
  background-color: @body_color;
}

h2
{
  color: @title_color;
  border: 1px solid @border_color;
}

You’ll notice that I avoided setting a value for @body_color because I didn’t want to override the value set previously in the theme. But then wouldn’t it be impossible for the theme to override the values for @title_color and @border_color? We seem to have a problem here.[1]

I want to be able to set some values and just use defaults for everything that I don’t want to override. There is a construct in SASS called !default that does exactly this. It indicates that an assignment should only take place if the variable has not yet been assigned.[2] Searching around for an equivalent in LESS took me to this page, Add support for “default” variables (similar to !default in SASS) #1706 (GitHub). There users suggested various solutions and the original poster became ever more adamant—“Suffice it to say that we believe we need default variable setting as we’ve proposed here”—until a LESS developer waded in to state that it would be “a pointless feature in less”, which seemed harsh until an example showed that he was quite right.

The clue is further down in one of the answers:

“If users define overrides after then it works as if it had a default on it. [T]hat’s because even in the imported file it will take the last definition in the same way as css, even if defined after usage. (Emphasis added.)”

It was at this point that the lightbulb went on for me. I was thinking like a programmer where a file is processed top-down and variable values can vary depending on location in the source text. That the output of the following C# code is 12 should amaze no one.

var a = 1;
Console.Write(a);
a = 2;
Console.Write(a);
a = 3;

In fact, we would totally expect our IDE to indicate that the value in the final assignment is never used and can be removed. Using LESS variable semantics, though, where variables are global in scope[3] and assignment are treated as they are in CSS, we would get 33 as output. Why? Because the value of the variable a has the value 3 because that’s the last value assigned to it. That is, LESS has a cascading approach to variable assignment.

This is exactly as the developer from LESS said: stop fighting it and just let LESS do what it does best. Do you want default values? Define the defaults first, then define your override values. The overridden value will be used even when used for setting the value of another default value that you didn’t even override.

Now let’s go fix our stylesheet to use these terse semantics of LESS. Here’s a first cut at a setup that feels pretty right. I put the files in the order that you would read them so that you can see the overridden values and everything makes sense again.[4]

theme-variables.less

@body_color: white;
@title_color: darken(@body_color, 25%);
@border_color: @title_color;

crimson.less

@import "theme-variables";
@body_color: #800;
@import "theme-base";

theme-base.less

body
{
  background-color: @body_color;
}

h2
{
  color: @title_color;
  border: 1px solid @border_color;
}

You can see in the example above that the required variables are all declared, then overridden and then used. From what we learned above, we know that the value of @title_color in the file theme-variables.less will use a value of #800 for @body_color because that was the last value it was assigned.

We can do better though. The example above hasn’t quite embraced the power of LESS fully. Let’s try again.

theme-base.less

@body_color: white;
@title_color: darken(@body_color, 25%);
@border_color: @title_color;

body
{
  background-color: @body_color;
}

h2
{
  color: @title_color;
  border: 1px solid @border_color;
}

crimson.less

@import "theme-base";
@body_color: #800;

Boom! That’s all you have to do. Set up everything in your base stylesheet file. Define all variables and define them in terms of each other in as convoluted a manner as you like. The final value of each value is determined before any CSS is generated.

This final version also has the added advantage that a syntax-checking IDE like JetBrains WebStorm or PHPStorm will be able to provide perfect assistance and validity checking. That wasn’t true at all for any of the previous versions, where variable declarations were in different files.

Although I was seriously considering moving away from LESS and over to SASS—because at least they didn’t leave out such a basic feature, as I had thought crossly to myself—I’m quite happy to have learned this lesson and am more happy with LESS than ever.


[1] For those of you who already know how to fix this, stop smirking. I’m writing this post because it wasn’t intuitive for me—although now I see the utter elegance of it.
[2] I’d also seen the same concept in NAnt property tasks where you can use the now-deprecated overwrite=“false” directive. For the curious, now you’re supposed to use unless=“${property::exists(‘property-name’)}” instead, which is just hideous.
[3] There are exceptions, but “variables are global in LESS is a good rule of thumb”. One example is that if a parameter for a mixin has the same name as a globally assigned variable, the value within that mixin is taken from the parameter rather than the global.
[4] Seriously, LESS experts, stop smirking. I’m taking a long time to get there because a programmer’s intuitive understanding of how variables work is a hard habit to break. Almost there.