Less.js: Add support for "default" variables (similar to !default in SASS)

Created on 3 Dec 2013  ·  35Comments  ·  Source: less/less.js

We are considering making the move from SASS to LESS, but the main obstacle for us is lack of a "default variable" feature (see http://sass-lang.com/documentation/file.SASS_REFERENCE.html#variable_defaults_)

This feature is incredibly useful when distributing your SASS code as a library where the variables serve as a sort of "API" for your users. In such a use case all the user has to do is make sure his variables are included up front to override the defaults that he wants to change, and voila! no fiddling with ordering of the (possibly) many files that may contain the variables that need overriding.

I've started working on an implementation, haven't written any tests yet but, hopefully we can use this pull request as a starting point for discussion: https://github.com/less/less.js/pull/1705

I chose the following syntax:

?foo: value;

as opposed to the SASS way:

@foo: value !default;

for 2 reasons - it's more concise, and the !default syntax may present potential problems with expression parsing on the right hand side in the future (but I could be wrong about that).

The implementation I came up with was surprisingly simple - hopefully I haven't missed anything important. I'd really appreciate any feedback you may have.

Cheers,
Phil

consider closing feature request low priority

Most helpful comment

I was going to suggest the article I wrote and then saw @seven-phases-max had linked to it. 😄 Trust us, what you're asking for already exists! But you have to understand Less's variable evaluation to understand how/why it exists.

All 35 comments

See The Documentation.


Update: Removed my previous comment of this post to not confuse future visitors (here I was trying to answer "how to define a variable if it's not already defined" question and missed the point it's an "XY Problem" and the correct answer for a Sass-like !default is: in Less you don't need anything like that because the necessary functionality ("(re)definition after use") is already provided by the way Less variables work).

If you define the same variable twice, the last declaration wins and is valid in the whole scope. So, another option would be to declare variables normally and ask user to include his variables after your library.

library.less

@width: 0;
.mixin {
  width: @width;
}

user.less:

@import "library.less"; //first declaration of @width
@width: 1; //this will override @width defined previously
.class {
  .mixin();
}

compiles into:

.mixin {
  width: 1;
}
.class {
  width: 1;
}

Thanks for the feedback on our PR. These solutions would be workable if we had a single thing or a few small things to consume. So let me elaborate on why these solutions are not workable in our case.

We make a large framework of components implemented as a class hierarchy. We declare the variables for each component in its own file... in our base theme. We then "derive" themes from here that want to modify this variables. Users can further do so to tune a theme to their needs.

Further, variables often "derive" from other variables. This most obvious is base-color. We have spent a good deal of time discussing alternatives to the problems of large class hierarchies of components using aggressive variable value propagation combined with themes and their own inheritance behavior.

By far the best solution to these problems is to _always_ define variables as "!default" (in Sass terms). This allows users to simply get in first and set their values before these values are used to calculate further values. The timing is then quite simple to manage for our users. Since this is always the case for every variable in all of our themes, syntax struggles like those suggested above would be quite a burden and also error prone.

We'd love to contribute other features to Less as we continue. I think our (unusually?) large scale use case could be a helpful validation of what is needed to scale up the language / feature set.

I hope you will consider merging this or provide some alternatives that are more declarative in nature. Exploiting scope tricks really won't work for us.

Thanks again for your time and consideration!

It seems you might be better off defining mixins instead of straight classes. Then, variables can be overridden late in your main stylesheet or another derived theme. That's how I'm able to override things like my gutter widths in my grids after I've imported them.

Just to clarify we are talking about variables here only. And we don't have a main stylesheet - we have one per class per theme. :)

Maybe I am not following your suggestion but as an example, one of our theme variable files looks like this:

$panel-base-color: $neutral-light-color !default;
$panel-header-color: $base-color !default;
$panel-frame-border-width: 1px !default;
$panel-header-font-size: round($font-size * 1.15) !default;
$panel-body-border-color: $neutral-dark-color !default;

$panel-light-header-color: #000 !default;
$panel-light-header-background-color: #fff !default;
$panel-light-tool-background-image: 'tools/tool-sprites-dark' !default;
$panel-light-body-border-color: $neutral-dark-color !default;
$panel-ignore-frame-padding: true !default;

This file sets up the various Panel values for a theme that has a chain of base themes 4 deep. Those base themes have their own variable values for Panel but these are loaded first so override them. Unless a user theme derives from this theme and provides values of their own.

This pattern is repeated _a lot_ :)

Okay, well why not just override @panel-base-color in your main less sheet? LESS variables are global so whichever one happens last is the winner.

@import 'theme.less';

@panel-base-color: red;

Now anywhere that's used in the theme will be overridden. If nobody overrides this down your import chain, then whatever it was originally set to will be the default.

We don't have a "main less sheet" :) I appreciate your help and suggestions, but we have had many discussions around this internally and they tend to go on for a bit. Suffice it to say that we believe we need default variable setting as we've proposed here. This feature exists in Sass and we found it very helpful in reducing complexity and providing users flexibility in configuring our themes.

I am curious if all this means that this pull request or some similar derivative would ever be accepted? We'd be happy to tweak the syntax if that is objectionable.

We don't have a "main less sheet" :)

Simply replace "your main less sheet" in answers above with "any of user's less sheets". So far it seems that SASS !default (whatever syntax it has) is invented to solve a problem that does not exist in LESS.
@dongryphon, I can be wrong of course, but you did not yet ground why you can't use the solution suggested by @SomMeri and @Soviut.

To put it another way, imagine if you weren't importing and instead had all your variables in the same sheet. That's effectively what importing does. So in that situation, if you had two variable declarations with the same name, the last one in the sheet would win.

@base-color: green;

div {
    background: @base-color;
}

@base-color: red;

Because base colour is declared again at the end, the base-color used in the div will be red. It will compile to:

div {
    background: red;
}

This came up before, in fact i believe it was even implemented and we had a pull request from guys who wanted it in bootstrap and they agreed after some discussion that it was a pointless feature in less..

The only thing it allows you to do is for users to define variables before importing. If users define overrides after then it works as if it had a default on it.. that's because even in the imported file it will take the last definition in the same way as css, even if defined after usage.

Its not that much of a burden to users of a library to say they override variables after imports (or import a variables file after imports) vs adding more syntax to less.. we think that is one of the adanvantages of less, that you can do the same amount as sass but most of the time with simpler syntax.

This is counter what a JavaScript programmer might think, but the idea behind it is that its closer to css.

Please can you elaborate on why asking consumers to consider order is not possible or desirable? We will accept features with strong usecases but we have to be rigorous to avoid language and complexity bloat.

See #1109 #1104 #313

And just to clarify, we're not saying "no" by any stretch. We're trying to show his this feature may already exist.

I added some info to less-docs

Closing as already available (in "Less way") feature (http://lesscss.org/features/#variables-feature-default-variables).

I think this confusion often comes up because while there is overlapping _syntax_, SASS "executes" from top-to-bottom but LESS does not, and instead operates more like CSS. LESS is like CSS+ (CSS with additional features), and SASS is like "PHP with CSS syntax". I wonder if there's a way (if necessary) we could make that distinction.

Please can you elaborate on why asking consumers to consider order is not possible or desirable? We will accept features with strong usecases but we have to be rigorous to avoid language and complexity bloat.

Because we teach library users (and consumers) to never edit library codes themselves. The missing !default feature means that we have to do one of those two things - both equally bad imo:

  • The users have to edit the library stylesheet themselves in order to edit the variables where they are declared (which is kind of a forbidden thing, making library updates harder) or,
  • The library has to provide two stylesheets : one for default variables and another one for actual rules. Then the user has to @import the first one, then declare its own variables, then @import the second one. It's more complex than it should be, especially to keep those two files at the same version.

The sass approach means that a library only has to provide a single file, that the user will then be able to customize.

my-color: red;
@import "./my-library.less";

Instead of:

@import "./my-library-variables.less";
my-color: red;
@import "./my-library-rules.less";

I think that you should reconsider this issue.

@arcanis Actually I can't see why you think that:

The library has to provide two stylesheets : one for default variables and another one for actual rules.
The sass approach means that a library only has to provide a single file

In Less it's absolutely the same library design:

@import "./my-library.less";
@my-color: red;

I guess you're simply missing the "Lazy-Loading" thing (and exactly the same example is used in the docs to say no, thanks! to !default).

I guess you're simply missing the "Lazy-Loading" thing

@seven-phases-max WTF, you're right. Dang it, I should probably delete my entire reply (and just did). You're telling me you can import bootstrap, and just override specific vars, and it will just work?

I didn't believe you, and did my own tests to verify it. How did I miss this?? I think it's because every article I've seen on Bootstrap, which included customizing, recommended that you copy the variables.less file and set your own values, which of course causes issues when more variables are added to the library. And I think I had the impression that selectors were being output at the immediate point of import. Did variables always "lazy load" in this way?

This has to be the single most important commonly missed feature in Less. I've never seen a blog post about Less or a Less library that mentions this feature. Even with everything in the thread here, and the documentation, it wasn't immediately obvious what that meant with real-world libraries. I thought everyone was simply saying you could override variables declared earlier by setting them later, and I never got that it would affect the evaluation of variables from earlier imported documents.

I can't believe I never got this until now. This changes basically everything about how I structure my less documents, and it has a tiny mention in the docs that doesn't demonstrate output.

Maybe the reason we get so many requests for a !default feature in Less is because few people intuit this feature or understand it from the brief example from the docs (including me, obviously).

At the very least, thanks for explaining it again @seven-phases-max!

Woops. Well, you're right. I misunderstood the docs too, my bad!

@Soviut you are a gosh-darn genius! I had no idea variable declarations work like that in SASS/LESS. Thank you!

I'm still slapping my forehead that this wasn't obvious behavior with everything else I've written in Less. I think my problem was that I'd seen poor example articles written by people who didn't understand how to do it.

Also, note: @spikesagal as far as your statement: "I had no idea variable declarations work like that in SASS/LESS" -- To the best of my knowledge, the behavior of variables is not the same between the two languages, since SASS and LESS evaluate things very differently.

And they are killing this feature in BS4 because of moving to Sass... :-1: Still pretty sad about that move.

So it begins, the fight for more !default variables: [twbs/bootstrap#17418]

Well, they cannot kill Less feature by changing SCSS sources in whatever way. (Technically it's not even a "feature" (like some sort of "synthesized behaviour") but a fundamental property/corollary of lazy-evaluation). As long as there's Less version of BS (that is just a matter of count of people willing to contribute to it), and there's at least one global variable - you will always be able to override that variable.

Well. The official v4-alpha of bootstrap moved to Sass. That, at least in my understanding, is killing less in their official documentation - and that means also killing the support for this feature because Sass has no such thing as lazy-loading variables. They only support !default, which, in a way, means: "Oh yeah, we allow you to override our variables just once from the outside, and only if we permit to do so. So, if we forget to give you access to a variable by simply omitting the !default, you're pretty much screwed and have to override the whole darn selector in your files. Deal with it."

and that means also killing the support for this feature because Sass has no such thing as lazy-loading variables.

Well, this only means "they kill it in Bootstrap" (which is obviously something out of this thread scope - as long as there's no Less version of BS, we should not care of whatever Bootstrap features in _this_ repository, should we?).

Since this discussion was pretty much about overriding variables in bootstrap...

We're sticking with less :smile:

I have a use case for the originally stated dilemma. Consider the following simplified example:

  1. I have a less file that defines the font size of h1, something like this
    @font_size__h1 = 30px;
  2. I have (for lack of a better phrase) a plugin that contains a separate LESS file that uses the @font_size__h1 declaration. So I @import (reference) "path/to/file.less"; it. No problem, it's now available.
  3. Now I take that "plugin" into a new site that does not have @font_size__h1 defined anywhere. At this point it would be great to say "If @font_size__h1 is defined, use that value. If it's not defined, then use the value I define here."

Item 3 is not currently possible, so far as I can see.

@theMikeD

Item 3 is not currently possible, so far as I can see.

It does not look like you've read the thread:

@import "path/to/file.less";
@import "here.less"; // if it's not defined <elsewhere>, then use the value I define <here>
@import "elsewhere.less";

which of course can be reduced to:

@import "path/to/file.less"; // <- define default value there
@font-size-h1: foo;          // if it's not defined here then use the value defined above

which is basically the same example as http://lesscss.org/features/#variables-feature-default-variables

Yeah I read the thread. Looks like you don't understand what I'm asking, because your example has my question backwards.

which of course can be reduced to:
@import "path/to/file.less"; // <- define default value there
@font-size-h1: foo; // if it's not defined here then use the value defined above

This is the opposite of what I need. This will overwrite the value of @font-size-h1 in path/to/file.less with the local value no matter what. What I need is the value in the local file only being used if:
a) path/to/file.less is not loaded, or
b) the value of '@ font_size__h1is not set inpath/to/file.less`

IOW the local value of @font-size-h1: foo; will always be present in the local file, but should be over-ridden if it's set in path/to/file.less

In any case, I found the solution last night, which is to assign the local value first, then put the @import statement at the end of the file, not the beginning. If found, it will overwrite the local value.

Thanks anyway.

Looks like you don't understand what I'm asking

I'd rather suggest you to start with some Less variable basics to refresh your view:

(because it seems you're just trying to think of it all in an imperative C/PHP-like manner, while in Less/CSS it's totally "declarative up-side-down").
It's been talked over to the death all over these years, !default can add _nothing_ new if compared to the native Less variable overriding. Period. (We've been there many times before: if one thinks he found some use-case for !default in Less, it means nothing but his misunderstanding of Less variable semantics).

What I need is the value in the local file only being used if:
a) path/to/file.less is not loaded, or
b) the value of @ font_size__h1 is not set in path/to/file.less

Then it's simply the opposite:

@font-size-h1: foo;
@import "path/to/file.less"; 

tada!

...which is where I landed, as I said.

I was going to suggest the article I wrote and then saw @seven-phases-max had linked to it. 😄 Trust us, what you're asking for already exists! But you have to understand Less's variable evaluation to understand how/why it exists.

I have a component - which is a data grid. This component should have a default styling - defined by the component package. But if a certain variable from outside is already defined that one should have priority.

app.less
/grid/grid.less

As the grid is a component, forget about adding anything here - or adding any code after the grid.less file.
I don't see how less covers this problem. Scss offers this feature for a good reason.

@geri777

But if a certain variable from outside is already defined that one should have priority.

Less evaluates like CSS.

.css {  
  --color: blue;
  color: var(--color);  // --color will be red
  --color: red;
  border-color: var(--color);  // --color will still be red, red is the scope's final value
}

.less {  
  @color: blue;
  color: @color;  // @color will be red
  @color: red;
  border-color: @color;  // @color will still be red, red is the scope's final value
}
````

Scss, instead, doesn't mimic CSS evaluation and instead evaluates more like, say, PHP.

```scss
.scss {  
  $color: blue;
  color: $color;  // $color will be blue
  $color: red;
  border-color: $color;  // $color will be red
}

So, in Sass/SCSS, in order to override a root variable's value, you are forced to do two things:

  1. You must tag all your variable declarations with !default
  2. You must insert your global vars before these default declarations.

As in:

// main.scss
@import "overrides.scss";
@import "library.scss";

// overrides.scss
$color: red;

// library.scss
$color: blue !default;

.scss {
  color: $color;
}

In Less, theming is much easier. You (typically) don't need to change anything about your library, you just need to put your overrides after. You only need to do one thing.

// main.less
@import "library.less";
@import "overrides.less";

// overrides.less
@color: red;

// library.less
@color: blue;

.less {
  color: @color;
}

Therefore, you do not need !default because Less will always accept your final value.

Think of Less evaluation like the CSS's cascade. It just works. The final declaration wins.

Was this page helpful?
0 / 5 - 0 ratings