Sunday, March 31, 2013

Less CSS - Tips and Trics

Less is declarative language designed to behave and look as closely to css as possible. Less started as an attempt to make css style sheets easier to read and maintain and then added more features and abilities. It evolved so much that it became theoretically possible to do anything with it.

This post shows some less known but very useful less features. It also contains list of potentially useful workarounds and tricks you can use if the default less syntax is not sufficient for what you need. Finally, last chapter shows one practical application of those tricks. We will create mixin to auto generate vendor prefixes for css declarations.

Table Of Contents

Basics

We assume that you know both what less is and its basic features. If you know how less variables, mixins and nesting work, then you know enough to read this post. If you never used them, you can start either with official documentation that belongs to original JavaScript compiler less.js or detailed language description written as part of java compiler less4j.

You can also read an introductory article about less written on this blog.

Compilers

Just few weeks ago, we released first non beta version of less compiler java port called less4j. Feel free to try it out.

Canonical compiler, the one whose behavior defines the language, is written in JavaScript and called less.js. It works both in browser and in node.js environment. We tried it with windows node.js and npm. The combination runs without hustle or problems.

Next less.js version 1.4.0 will be backward incompatible, but for good reasons. Changes are meant to solve some old quirks and will be partially described in our next post. Of course, less4j will follow less.js changes as soon as possible.

Lists of other ports and GUI editors are maintained on less.js wiki page.

Less Known Less Abilities

This chapter contains four less features. Three of them have been added only recently and one was documented only in less.js issue database.

The list of features covered by this chapter:
Mixins With Return Values
Variables declared in mixin can act as return values. This is important not only because it can be useful occasionally, but also because this feature has one rather important gotcha.

Variables act as mixins return values:
.mixin() { @unique: 45; } /* mixin defines variable */

#call-mixin {
  variable-value: @unique; /* print variable defined inside mixin */
  .mixin(); /* call the mixin */ 
}

All variables defined in mixin are copied into callers scope and can rewrite local variables. In addition, any mixin can call other mixins and those other mixins can call yet another mixins. All that can create an arbitrary long chain of mixins calls. Any of them may modify value of any variable defined in the original caller scope.

The dangerous part "can rewrite local variables" will be fixed in the next 1.4.0 version. Next less.js release will copy only those variables, that can not possibly rewrite local values.

Consider following less:
.mixin() { @clash: 45; @unique: 45; } /* mixin defines two variables */

#call-mixin {
  @clash: 0; /* variable with the same name as the one inside .mixin */
  /* print actual variables values */
  unique: @unique; 
  clash: @clash;
  .mixin(); /* call the mixin */ 
}

compiled with less 1.3.3:
#call-mixin {
  unique: 45; /* variable defined inside mixin was accessible */
  clash: 45; /* variable defined inside mixin rewrote local value */
}

compiled with less 1.4.0:
#call-mixin {
  unique: 45; /* variable defined inside mixin was accessible */
  clash: 0; /* local variable kept its value*/
}

Note: less 1.4.0 will protect only local variables. Variables found in any other scope, for example global scope, are not protected.
Click to expand:

Non local variable is not protected:
/* mixin with return value */
.mixin() { @clash: 45; } 

/* variable with the same name .mixin return value */
@clash: 0; 

#call-mixin {
  clash: @clash; /* print variables value */
  .mixin(); /* call the mixin */ 
}

less 1.4.0 compilation result:
#call-mixin {
  /* variable defined outside of local scope was not protected */
  clash: 45; 
}

Semicolon as Mixin Arguments Separator
Less.js 1.3.3 added semicolon as mixin arguments separator. Both .call(1; 2; 3) and .call(1, 2, 3) are now valid mixin calls and do the same thing. Mixin call with semicolon separated arguments may look weird and unusual, but there was good reason for this addition.

The symbol comma has meaning in standard css. It is list separator and 1, 2, 3 is valid list containing three numbers. Comma acting as mixin arguments separator made it impossible to use comma separated lists as arguments.

If the latest less compiler sees at least one semicolon inside mixin call or declaration, then it assumes that arguments are separated by semicolons and all commas belong to css lists:
/* two arguments, both with comma separated list */
.call(1, 2, 3; something, else);
/* three arguments and each contains one number */
.call(1, 2, 3);
/* one argument containing comma separated css list */
.call(1, 2, 3;); // note dummy semicolon 

/*comma separated default value */
.declaration(@param1: red, blue;) {
  // mixin content
}

Media Query in Variable
Last releases of less have ability to store media queries in variables and use those variables inside @media rules. Use it whenever you need the same media query at multiple places. If you decide to modify that media query, you will have only one place to change.

Store and use tablet and (min-width: 500px) media query:
/* note '~' in the beginning - media query must be escaped */
@singleQuery: ~"tablet and (min-width: 500px)";
@media screen, @singleQuery {
  set {
    padding: 3 3 3 3;
  }
}

compiles into:
@media screen, tablet and (min-width: 500px) {
  set {
    padding: 3 3 3 3;
  }
}

Import
Less import statement is not limited to style sheet top level. It is possible to import less files into rulesets and mixins. The feature helps to deal with name conflicts between multiple less libraries or less libraries versions.

.namespace-1-2-3() {
  @import "library-1.2.3.less";
}
.namespace-2-0-0() {
  @import "library-2.0.0.less";
}
#use-both-libraries-without-name-conflicts {
  .namespace-1-2-3 > .usefull-mixin();
  .namespace-2-0-0 > .usefull-mixin();
}

Tricks and Workarounds

Some occasionally demanded but not yet directly supported features are achievable if you are willing to use a workaround. This chapter shows three such tricks:
Triple Variable Indirection
Related less.js issue: Variable Name Interpolation

Used less features:
  • variables,
  • variable references,
  • string interpolation.

What Works
Less supports variables and any variable holding string can act as reference to another variable. If you want to use references, you have to put additional @ before the variable name e.g., @@reference.

Use the @reference variable as a reference to the @number variable:
@number: 10;
@reference: "number"; 
h1 { size: @@reference; } 

output:
h1 { size: 10; } 

What Does Not Work
Less does not support triple variable indirection. This @@@referenceToReference is not valid less:
@number: 10;
@reference: "number"; 
@referenceToReference: "reference"; // desperate attempt
h1 { size: @@@referenceToReference; } //syntax error

Workaround
Use string interpolation to compose referenced variable name:
@number: 10;
@reference: "number"; 
/* compose indirect variable name inside another variable */
@referenceToReference: "@{reference}"; 
h1 { size: @@referenceToReference; } 

previous less compiles into following css:
h1 { size: 10; }

Bonus: string interpolation allows you to go arbitrary far:
@number: 10;
@reference: "number"; 
@referenceToReference: "@{reference}"; 
/* chaining of interpolated variables can be arbitrary long */
@referenceToReference2: "@{referenceToReference}"; 
@referenceToReference3: "@{referenceToReference2}"; 
@referenceToReference4: "@{referenceToReference3}"; 
@referenceToReference5: "@{referenceToReference4}"; 
h1 { size: @@referenceToReference5; }

Interpolate Property Names
Related less.js issue: Auto Prefix replacement

Used less features:
  • escaped values,
  • variables.

What Works
Any content can be stored into variable and then used
  • as property value,
  • inside string or escaped value,
  • as selector,
  • as media query.

Click to expand:

Sample input:
@property_value: 1px;
@selector: ~"div.someclass";
@media_query: ~"tablet";

@media @media_query {
  @{selector} {
    padding: @property_value;
    in-string: "Padding was set to: @{property_value}";
  }
}

compiles into:
@media tablet {
  div.someclass {
    padding: 1px;
    in-string: "Padding was set to: 1px";
  }
}

What Does Not Work
Property names can not be rendered dynamically. You can not use variables or escaped values to generate property names:
@css-property-name: "size"; 
#desperate-1 { 
  @css-property-name: 10; // syntax error 
}  
#desperate-2 { 
  @{css-property-name}: 10; // syntax error 
}

Workaround
Browsers ignore declarations with an unknown property. The declaration hack: 1; will be ignored by all browsers, so we do not have to care whether generated css contains it or not.

In addition, less escaping allows you to place any content directly into generated css. Escaped values use ~"escaped value" syntax and the intention was to allow browser hacks, invalid css or proprietary syntax otherwise unrecognized by Less. Regardless of original intentions, this hack: 1 ~"; something" will be rendered as hack: 1 ; something, so we can abuse it.

Render property name from a variable:
@css-property-name: "size"; 
#workaround { 
  hack: 1 ~"; @{css-property-name}:" 10; 
}

previous less compiles into following css:
#workaround {
  hack: 1 ; size: 10;
}

Looping
Related less.js issue: Idea: loop support

Used less features:
  • mixins,
  • guards,
  • expressions.

What Does Not Work
Less is declarative language and does not support looping. It does not have native while, for, repeat ... until or any other cycling structure.

Workaround
Use recursive mixins calls to simulate the loop. The most simple looping mixin needs only one parameter - how many times should it run the loop. It does whatever it needs to do and then calls itself with smaller parameter. Looping stops once the parameter reaches zero.

Any cycling structure can be simulated this or similar way, but you have to be careful. It is very easy to write forever running mixin.

Create looping mixin:
/* looping mixin */
.loop (@index) when (@index > 0) { 
  loop: member @index; /* do whatever you need to do */
  .loop((@index - 1)); /* loop again */
}
/* stop mixin */
.loop (@index) when (@index <= 0) { 
  loop: end;
}

/* use looping mixin */
#run-loop-5-times {
  .loop(5);
}

compiles into:
#run-loop-5-times {
  loop: member 5;
  loop: member 4;
  loop: member 3;
  loop: member 2;
  loop: member 1;
  loop: end;
}

Note: we used double parentheses in .loop((@index - 1)) call. This was intentional and not a mistake. Future less 1.4.0 will require parentheses around math operation, so we added them to have future-proof code example.

Who Would Use That
Twitter Bootstrap uses looping to generate the grid:
Click to expand:

/* Simplified Twitter Bootstrap Code */
.core (@gridColumns, @gridColumnWidth) {

  .spanX (@index) when (@index > 0) { /* looping mixin */
    (~".span@{index}") { .span(@index); } /* generate column */
    .spanX(@index - 1); /* loop again */
  }
  .spanX (0) {} /* stop mixin */

  .span (@columnIndx) {
    width: (@gridColumnWidth * @columnIndx);
  }

  // generate .spanX and .offsetX
  .spanX (@gridColumns);
}

/* generate the grid */
.sample-grid {
  @gridColumns: 3;
  @gridColumnWidth: 55;
  .core(@gridColumns, @gridColumnWidth);
}

compiles into:
.sample-grid .span3 {
  width: 165;
}
.sample-grid .span2 {
  width: 110;
}
.sample-grid .span1 {
  width: 55;
}

Example: Loop Through List
Mixin to loop through list of strings or escaped values does not end when the index reaches zero, it checks type of the next list member instead:
.loop-strings(@list, @index: 1) when (isstring(extract(@list, @index))) {
  @currentMember: extract(@list, @index);
  do-something-with: @currentMember;
  .loop-strings(@list, (@index + 1)); /* loop the next member */
}

#use-place {
  // loop through list of escaped values
  .loop-strings(~"A", ~"B", ~"C", ~"1", ~"2", ~"3";);
}

compiles into:
#use-place {
  do-something-with: A;
  do-something-with: B;
  do-something-with: C;
  do-something-with: 1;
  do-something-with: 2;
  do-something-with: 3;
}

Generate Vendor Prefixes

Auto generation of vendor prefixes in css declarations is one of those things that are repeatedly asked for in less.js issues database. While less currently does not have such feature, it is possible to combine above workarounds into mixin that auto-generates them.

API
The mixin .vendorPrefixes(@prefixes, @declaration, @values...) has two mandatory arguments followed by any number of optional arguments:
  • @prefixes - mandatory list of prefixes,
  • @declaration - mandatory string with the declared property,
  • @values... - optional - all declared values.

This solution is not perfect, the declaration to be rendered has to be written inside string and sent as mixin parameters:
.vendorPrefixes(@list-of-prefixes; "transition"; all 4s ease);

That call has certainly lower then ideal readability. On the other hand, this solution shows full power of less and combines two different tricks into one solution, so it still can be useful for demonstration purposes.

How It Works
Rendering mixin uses interpolate property names trick to render vendor prefixed declaration:
/* Render exactly vendor prefixed declaration. */
.renderWithPrefix(@prefix; @declaration; @values...) {
  hack: 1 ~"; @{prefix}-@{declaration}: " @values;
}

Looping mixin goes through all prefixes:
.nthVendorPrefix(@prefixes; @index; @declaration; @values...) 
when (isstring(extract(@prefixes, @index))) {
  @prefix: extract(@prefixes, @index);
  .renderWithPrefix(@prefix, @declaration, @values);
  .nthVendorPrefix(@prefixes; (@index + 1); @declaration; @values);
}

Finally, "API" mixin to starts the looping-rendering process:
.vendorPrefixes(@prefixes, @declaration, @values...) {
  .nthVendorPrefix(@prefixes; 1; @declaration; @values);
}

Full Code
/* Render exactly vendor prefixed declaration. */
.renderWithPrefix(@prefix; @declaration; @values...) {
  hack: 1 ~"; @{prefix}-@{declaration}: " @values;
}

/* Looping mixin assumes that @prefixes list contains strings:
   * finds @index-th prefix in @prefixes list,
   * renders one vendor prefixed declaration,
   * loop: call itself with higher @index.

  Guard prevents indefinite looping, it will stop once @index goes off the list.
*/
.nthVendorPrefix(@prefixes; @index; @declaration; @values...) 
when (isstring(extract(@prefixes, @index))) {
  @prefix: extract(@prefixes, @index);
  .renderWithPrefix(@prefix, @declaration, @values);
  .nthVendorPrefix(@prefixes; (@index + 1); @declaration; @values);
}

/* Render exactly vendor prefixed declaration. */
.vendorPrefixes(@prefixes, @declaration, @values...) {
  .nthVendorPrefix(@prefixes; 1; @declaration; @values);
}

#use-place {
  @list-of-prefixes: ~"-moz", ~"-ie", ~"-webkit";
  .vendorPrefixes(@list-of-prefixes; "transition"; all 4s ease);
}

compiles into:
#use-place {
  hack: 1 ; -moz-transition:  all 4s ease;
  hack: 1 ; -ie-transition:  all 4s ease;
  hack: 1 ; -webkit-transition:  all 4s ease;
}

To Be Continued

Next post is about less expressions and their planned backward incompatible changes.

In most cases, expressions are easy to use and very useful. Unfortunately, their interactions with the rest of the css syntax can occasionally cause somewhat quirky behavior. Next post shows when it happens, what will be solved by planned changes and which traps will still remain there.

5 comments:

Brian Rinaldi said...

Maria,

Good post! I run a site for web devs called Flippin' Awesome. Wondering if you might be interested in letting me reprint these on there and hopefully bring them to a wider audience. Email be at brinaldi [at] adobe [dot] com if you are interested.

- Brian

ellenh said...

Many thanks for your work. I'm looking into Less and yours is the best reference and explanation I've found. Better than the very incomplete documentation on lesscss.org. Beats even "real" tutorial books on the subject.

Meri said...

@ellenh thank you. If you want, you can check also less4j documentation.

Less4j is java port of less.js and its wiki contains more details then this post. The wiki was written as the compiler was developed and aimed to contain as many details as possible.

Benjamin Gleitzman said...

Escaped selector interpolation is deprecated in 1.4.x.

Instead of
~".span@{index}"

Instead use
.span@{index}

Jes Per said...

For a quick way to learn css and also generate it I recommend something like http://www.generatecss.com. But your tips have some truth in them as well.

Post a Comment