Using variables in property names in LESS (dynamic properties / property name interpolation)

I noticed that inuit.css, which was written in SASS, has a .vendor mix-in:

@mixin vendor($property, $value...){
    -webkit-#{$property}:$value;
       -moz-#{$property}:$value;
        -ms-#{$property}:$value;
         -o-#{$property}:$value;
            #{$property}:$value;
}

Is there a way to replicate this in LESS with some of the odd features like e() and @{ } ?


Solution 1:

Update: LESS >= 1.6

As of version 1.6 (see changelog) property name interpolation is implemented in LESS. So you don't need any magic anymore. (For older versions, see my original answer.)

Your mixin would work basically as is:

LESS:

.vendor(@property; @value){
    -webkit-@{property}: @value;
       -moz-@{property}: @value;
        -ms-@{property}: @value;
         -o-@{property}: @value;
            @{property}: @value;
}

/*example*/
.test {
    .vendor(transform, translateX(20px));
}

CSS:

.test {
  -webkit-transform: translateX(20px);
  -moz-transform: translateX(20px);
  -ms-transform: translateX(20px);
  -o-transform: translateX(20px);
  transform: translateX(20px);
}

Original answer: LESS < 1.6

As far as I'm concerned less hasn't added support for dynamically inserted properties, which has been previously discussed on SO many times, see maybe:

  • LESS CSS Escape entire CSS rule with different prefixes?

  • Generic `vendors` mixin

  • Rounded corner tables with LESS

So the way it is usually done would be with parametric mixins and pattern matching ... so it is a little bit more hard coding ... but properties and different vendors anyway sometimes require a little different format of parameters, so a little more control is added this way.

Workaround #1: inject dynamicaly generated properties into a properties value

The first option for a workaround is a bit ugly but I tried it and it worked on http://less2css.org. So, what I tried was to inject the dynamically created properties into a value of another property that you hard code (which I just gave a random "vendor" name -inj here and assigned it value ect, but you might want to use something usefull instead if you already add an element in from of all vendor mixin includes)

.vendors(@property, @value, @pre: ect) {
    -inj:~"@{pre}; -webkit-@{property}: @{value}; -moz-@{property}: @{value}; -ms-@{property}: @{value}; -o-@{property}: @{value}; @{property}: @{value}";
}

We can try it on an example - maybe something to make it worth ... lets try transform short:

LESS:

.test-class{
    .vendors(transform, matrix(1,0,0,1,20,20));
    .vendors(transform-origin,"10px 10px");
}

CSS output:

.test-class {
    -inj: ect; -webkit-transform: matrix(1, 0, 0, 1, 20, 20); -moz-transform: matrix(1, 0, 0, 1, 20, 20); -ms-transform: matrix(1, 0, 0, 1, 20, 20); -o-transform: matrix(1, 0, 0, 1, 20, 20); transform: matrix(1, 0, 0, 1, 20, 20);
    -inj: ect; -webkit-transform-origin: 10px 10px; -moz-transform-origin: 10px 10px; -ms-transform-origin: 10px 10px; -o-transform-origin: 10px 10px; transform-origin: 10px 10px;
}

This seems to produce working css but it feels kinnda wrong =)


Workaround #2: inject dynamicaly generated properties into the name of the folowing class (up to v1.3.3)

So I played with this idea a little more ... and thought of a way that does not produce unnecessary properties. It injects the dynamically created properties into the name of the next class. Let me show you how I got it to work:

1) Let us define a general vendor mixin (the @rest argument will be used to line up multiple vendor blocks later)

.vendors(@property, @value, @rest:"") {
    @inject:~"@{rest} -webkit-@{property}: @{value}; -moz-@{property}: @{value}; -ms-@{property}: @{value}; -o-@{property}: @{value}; @{property}: @{value};";
}

2) Construct a virtual class/ruleset that we want the vendors to be included in, but we make it as a mixin that at the end constructs the next class aswel - so we really make a mixin that will recursively build two or more classes. For example (using the same example as above) we can write something like this (notice the use of @inject in the second .vendor() call to bind the two vendor blocks together):

.test(@nextclass){
    .vendors(transform, "matrix(2,0,0,2,20,20)");
    .vendors(transform-origin,"10px 10px", @inject);
    (~"{@{inject}} .@{nextclass}"){/*next class properties*/};
}

3) Now just we wrap this mixin into another class to display in the css:

.this-class{
    .test(next-class);
}

The resulting CSS will include this:

.this-class {
  -webkit-transform: matrix(2, 0, 0, 2, 20, 20);
  -moz-transform: matrix(2, 0, 0, 2, 20, 20);
  -ms-transform: matrix(2, 0, 0, 2, 20, 20);
  -o-transform: matrix(2, 0, 0, 2, 20, 20);
  transform: matrix(2, 0, 0, 2, 20, 20);
  -webkit-transform-origin: 10px 10px;
  -moz-transform-origin: 10px 10px;
  -ms-transform-origin: 10px 10px;
  -o-transform-origin: 10px 10px;
  transform-origin: 10px 10px;
}
.next-class {
  /*next class properties*/
}

The output will be just all in one line.

Edit: For nicer formatting you can include javascript interpolation of "\n" and "\t", see Scott's suggestion in comments below.

This way you can now chain up multiple classes all using the vendor mixin, without any unnecessary properties.


Workaround #3: inject dynamicaly generated properties into the name of the folowing class (v1.4.0) using recursion

I am adding this cause Scott pointed out in one of the comments that the changes that are coming with version 1.4 won't allow the workaround #2. But if we are a little resourceful, we can overcome this this problem. Let us see what are the problems of above workaround and fix them.

1) The first problem would be that the "(~".@{index}") { ... selector interpolation is deprecated", so we need to do the string interpolation in a separate step. Implementing this would be enough to inject a single .vendors mixin from above.

LESS: (using Scott's newline suggestion):

@nl: `"\n\t"`;        
.vendors(@property, @value) {
    @inject:~"@{nl}-webkit-@{property}: @{value};@{nl}-moz-@{property}: @{value};@{nl}-ms-@{property}: @{value};@{nl}-o-@{property}: @{value};@{nl}@{property}: @{value};";
}
.test(@nextclass){
    .vendors(transform, "matrix(2,0,0,2,20,20)");
    @inj: ~"{@{inject}`'\n'`} `'\n'`.@{nextclass}";
    @{inj} {/*next class properties*/}
}
.this-class{
    .test(next-class);
}

CSS output:

.this-class {
    -webkit-transform: matrix(2,0,0,2,20,20);
    -moz-transform: matrix(2,0,0,2,20,20);
    -ms-transform: matrix(2,0,0,2,20,20);
    -o-transform: matrix(2,0,0,2,20,20);
    transform: matrix(2,0,0,2,20,20);
} 
.next-class {
    /*next class properties*/
}

2) The second problem would be that "variables in mixins no longer 'leak' into their calling scope", but I noticed in the 1.4.0 beta, that if a variable is only introduced in a mixin it still can be called from the including ruleset, so with a little recursion, you could construct the .vendors blocks, and in the last step assign them to a new variable, that you then use for injection. I also got excited and used the new extract() function introduced in this version of less. With variable @i we assign the level of recursion (number of vendor blocks to be injected).

LESS:

@nl: `"\n\t"`; 
.multi(@props,@vals,1,@inj) {
    @property: extract(@props, 1);
    @value: extract(@vals, 1);
    @inject:~"@{inj}@{nl}-webkit-@{property}: @{value};@{nl}-moz-@{property}: @{value};@{nl}-ms-@{property}: @{value};@{nl}-o-@{property}: @{value};@{nl}@{property}: @{value};";
}

.multi(@props,@vals,@i,@inj:"") when (@i > 0) {
    @property: extract(@props, @i);
    @value: extract(@vals, @i);
    @injnext:~"@{inj}@{nl}-webkit-@{property}: @{value};@{nl}-moz-@{property}: @{value};@{nl}-ms-@{property}: @{value};@{nl}-o-@{property}: @{value};@{nl}@{property}: @{value};";
    .multi(@props,@vals,(@i - 1),@injnext);
}

@properties: "transform-origin" "transform";
@values: "10px 10px" "matrix(2,0,0,2,20,20)";

// string of other properties you want to include in the same class
@p: ~"@{nl}width:20px; @{nl}height:12px; @{nl}background-color:#000;";

.this-class {
    .multi(@properties,@values,2,@p);
    @inj: ~"{@{inject}`'\n'`} `'\n'`.next-class ";
    @{inj} {/**/}
}

CSS output:

.this-class {
  width:20px;
  height:12px;
  background-color:#000;
  -webkit-transform: matrix(2, 0, 0, 2, 20, 20);
  -moz-transform: matrix(2, 0, 0, 2, 20, 20);
  -ms-transform: matrix(2, 0, 0, 2, 20, 20);
  -o-transform: matrix(2, 0, 0, 2, 20, 20);
  transform: matrix(2, 0, 0, 2, 20, 20);
  -webkit-transform-origin: 10px 10px;
  -moz-transform-origin: 10px 10px;
  -ms-transform-origin: 10px 10px;
  -o-transform-origin: 10px 10px;
  transform-origin: 10px 10px;
}
.next-class {
  /*next class properties*/
}

Now this worked pretty well for me in 1.4.0 beta, but let us see what the future brings.

Solution 2:

I just wanted to add that you can use 'minus' as a propery name and the parser will ignore it but add the rest of the string. That way you won't get an empty inject:; or inj propery. It's still hacky but hey... :)

.prefix(@property, @value) {
    -:~";-webkit-@{property}: @{value}; -moz-@{property}: @{value}; -ms-@{property}: @{value}; -o-@{property}: @{value}; @{property}: @{value}";
}

Example:

.prefix(transition, "all .2s, color 0s");

Will output:

-webkit-transition: all .2s, color 0;
-moz-transition: all .2s, color 0;
-ms-transition: all .2s, color 0;
-o-transition: all .2s, color 0;
transition: all .2s, color 0;