18

I'm introducing LESS to a large web app project to simplify my CSS. I've got a few CSS rules which apply transitions to a varying number of properties, for example:

.movable {
    transition-property: top, left;
    transition-duration: 0.2s;
    transition-timing-function: ease;
}

.fadeAndStretchable {
    transition-property: opacity, width, height, margin;
    transition-duration: 1.5s;
    transition-timing-function: ease-out;
}

(Note: I've omitted -webkit, -moz and -o properties here for brevity: in reality each of these rules is 12 lines long rather than 3.)

Note that the values for transition-property are comma-separated. This is unusual in CSS: multiple values are usually space-separated (as in border: 1px solid #f00). LESS mixins can use the special @arguments value to produce a space-separated list of all the mixin arguments - but is it possible to define a LESS mixin that takes a variable number of parameters and turns them into a comma-separated value list, suitable for transition-property?

If necessary, I'm happy with a solution that requires two mixins: one for transition-property and another for transition-duration and transition-timing-function. Here's what I've tried so far:

Attempt 1: using @arguments with unnamed parameters

.transition-property() {
    -webkit-transition-property: @arguments;
    -moz-transition-property: @arguments;
    -o-transition-property: @arguments;
    transition-property: @arguments;
}

.movable {
    .transition-property(top, left);
}

Result: LESS error ("No matching definition was found for '.transition-property(top, left)'")

Attempt 2: using @arguments with named parameters

.transition-property(@p1, @p2, @p3, @p4, @p5) {
    -webkit-transition-property: @arguments;
    -moz-transition-property: @arguments;
    -o-transition-property: @arguments;
    transition-property: @arguments;
}

.movable {
    .transition-property(top, left);
}

Result: LESS error ("No matching definition was found for '.transition-property(top, left)'")

Attempt 3: using named parameters with dummy default values

.transition-property(@p1:p1, @p2:p2, @p3:p3, @p4:p4, @p5:p5) {
    -webkit-transition-property: @p1, @p2, @p3, @p4, @p5;
    -moz-transition-property:  @p1, @p2, @p3, @p4, @p5;
    -o-transition-property:  @p1, @p2, @p3, @p4, @p5;
    transition-property:  @p1, @p2, @p3, @p4, @p5;
}

.movable {
    .transition-property(top, left);
}

Result: No LESS error but it generates a CSS rule -webkit-transition-property: top, left, p3, p4, p5 that the browser ignores because of the unrecognised properties.

I've tried various other approaches (e.g. passing the property as a string 'top,left') but all result in the same thing: either a LESS error or invalid CSS.

Is there any way round this? Or do I have to bite the bullet and define a set of mixins overloaded on arity, e.g.

.transition-property(@p1) {...}
.transition-property(@p1, @p2) {...}
.transition-property(@p1, @p2, @p3) {...}
.transition-property(@p1, @p2, @p3, @p4) {...}
etc.
Community
  • 1
  • 1
Mark Whitaker
  • 8,465
  • 8
  • 44
  • 68

4 Answers4

30

I've managed to figure it out thanks to Luke Page pointing me towards the ... syntax.

The solution was to use the following:

Phew. Here's the resulting mixin:

.transition-properties(...) {
    -webkit-transition-property: ~`"@{arguments}".replace(/[\[\]]/g, '')`;
}

And here's the full version with a complete set of browser extensions:

.transition-properties(...) {
    @props: ~`"@{arguments}".replace(/[\[\]]/g, '')`;
    -webkit-transition-property: @props;
    -moz-transition-property: @props;
    -o-transition-property: @props;
    transition-property: @props;
}
Community
  • 1
  • 1
Mark Whitaker
  • 8,465
  • 8
  • 44
  • 68
  • Out of interest what do you need the JavaScript for? What's inserting square brackets? – Luke Page May 03 '12 at 06:08
  • 1
    `@arguments` is an array. If you just use it bare in LESS you get all the elements separated by spaces. But for this property (unusually) they need to be comma separated. If you use variable interpolation on `@arguments` you get a string containing a comma-separated list in square brackets (like a JavaScript array literal): `"[a,b,c]"` – Mark Whitaker May 03 '12 at 06:17
  • Sometimes we want a different @props for each browser, as it is the case if you need to animate a transformation. You must do `-webkit-transition-property: -webkit-transform`, `-moz-transition-property: -moz-transform` and so on. This can be done with `-webkit-transition-property: replace(@props, 'XX', '-webkit-', 'g');`, `-moz-transition-property: replace(@props, 'XX', '-moz-', 'g');` and so on. Then simply call with `.transition-properties('XXtransform', 'color')` – Jose Rui Santos Sep 24 '14 at 06:45
  • I meant to call with `.transition-properties(XXtransform, color)` – Jose Rui Santos Sep 24 '14 at 06:55
  • the weird thing is that when you pass a single argument, it results in a comma seperated string (opacity 0.5s) would yield (opacity, 0.5s), why? – Ayyash Dec 30 '14 at 07:11
21

Perhaps I am misunderstanding your needs. Why can you not use an escaped string?

Like so:

.transition ( @property, @duration, @style: ease-in-out ) {
  -webkit-transition-property: @property;  
  -webkit-transition-duration: @duration;
  -webkit-transition-timing-function: @style;

  -moz-transition-property: @property;  
  -moz-transition-duration: @duration;
  -moz-transition-timing-function: @style;

  -ms-transition-property: @property;  
  -ms-transition-duration: @duration;
  -ms-transition-timing-function: @style;

  -o-transition-property: @property;  
  -o-transition-duration: @duration;
  -o-transition-timing-function: @style;

  transition-property: @property;  
  transition-duration: @duration;
  transition-timing-function: @style;
}

#my-id {
  .transition( ~"background, border-color, color", 2s );
}

This is exactly what we use for multi-property transitions. Never had a problem with it.

Spot
  • 7,962
  • 9
  • 46
  • 55
  • 1
    I'd up-vote this answer 10 times if I could. Simple and very useful. I previously had no idea about escaped strings in Less. Thanks. – spikyjt Nov 13 '13 at 14:29
  • @spikyjt It took me a bit to find it, as well. Glad it helps. – Spot Nov 13 '13 at 15:12
  • What does the tilde mean? `~""` – Redsandro Jan 07 '14 at 20:43
  • 1
    @Redsandro It tells the LESS parser to not escape the following content. Therefore it passes it as-is. For more info, goto [link](http://lesscss.org) and do a page search for `Escaping`. You'll see it immediately. – Spot Jan 09 '14 at 08:37
  • 1
    The downside of this method is because it is escaped you can't use LESS variables within the strings. I use variables for the transition duration, and this function cannot handle multiple transition durations. Also, the new documentation for escaping is now [here](http://lesscss.org/functions/#string-functions-e). – 0x24a537r9 Jun 12 '14 at 01:18
  • @0x24a537r9 You absolutely can, i.e: `height:~"calc(100% - " @widget-header-height + 2px ~")";`. Maybe this is a new feature, though. – tfrascaroli Apr 25 '16 at 15:10
7

Flexible (LESS 1.5.1+)

This solution does not use any inline javascript and it allows for:

  1. Defaults to be set
  2. Any number of properties, durations, delays, etc., to be passed
  3. Output either in long form or compact form
  4. A raw list input instead of groups of parameters being input if desired

If the number of properties are greater than the number of durations, delays, or timings, then if the compact output is set, the final value for duration/delay/timing becomes the value for that parameter for all additional properties beyond the number passed, but if compact is not set, then the long form is output and values are duplicated per browsers interpret ion of the css standards.

LESS Mixin

.transition (@props: all; 
             @duration:1s; 
             @delay: 0s; 
             @timing: ease; 
             @compact: true;
             @raw-input: false) {
  .output() when (@raw-input = false) and not (@compact = true) {
  -webkit-transition-property:@props; 
     -moz-transition-property:@props;
      -ms-transition-property:@props;
       -o-transition-property:@props; 
          transition-property:@props;
  -webkit-transition-duration:@duration; 
     -moz-transition-duration:@duration;
      -ms-transition-duration:@duration;
       -o-transition-duration:@duration; 
          transition-duration:@duration;
  -webkit-transition-delay:   @delay; 
     -moz-transition-delay:   @delay;
      -ms-transition-delay:   @delay;
       -o-transition-delay:   @delay; 
          transition-delay:   @delay;
  -webkit-transition-timing-function:@timing; 
     -moz-transition-timing-function:@timing;
      -ms-transition-timing-function:@timing;
       -o-transition-timing-function:@timing; 
          transition-timing-function:@timing;
  }
  .output() when (@raw-input = false) and (@compact = true) {
    @propsLength: length(@props);
    @durationLength: length(@duration);
    @delayLength: length(@delay);
    @timingLength: length(@timing);
    .buildString(@i, @s: ~'') when (@i <= @propsLength) {
      @prop: extract(@props, @i);
      .setDuration() when (@i <= @durationLength) {
        @dur: extract(@duration, @i);
      }
      .setDuration() when (@i > @durationLength) {
        @dur: extract(@duration, @durationLength);
      }
      .setDuration();
      .setDelay() when (@i <= @delayLength) {
        @del: extract(@delay, @i);
      }
      .setDelay() when (@i > @delayLength) {
        @del: extract(@delay, @delayLength);
      }
      .setDelay();
      .setTiming() when (@i <= @timingLength) {
        @time: extract(@timing, @i);
      }
      .setTiming() when (@i > @timingLength) {
        @time: extract(@timing, @timingLength);
      }
      .setTiming();
      .setDivider() when (@i > 1) {
        @divider: ~'@{s},';
      }
      .setDivider() when (@i = 1) {
        @divider: ~'';
      }
      .setDivider();
      @string: @divider @prop @dur @del @time;
      .buildString((@i + 1), @string);  
    }
    .buildString(1);
    .buildString(@i, @s: ~'') when (@i > @propsLength) {
      .compact(@s);
    }
  }
  .output() when not (@raw-input = false) {
    .compact(@raw-input);
  }
  .compact(@string) {
    -webkit-transition:@string; 
       -moz-transition:@string;
        -ms-transition:@string;
         -o-transition:@string; 
            transition:@string;    
  }
  .output();
} 

LESS Use Examples

.test {
  .transition();
}
.test-props {
  .transition(width);
}
.test-duration {
  .transition(@duration: 3s);
}
.test-delay {
  .transition(@delay: 10s);
}
.test-timing {
  .transition(@timing: linear);
}
.test-all {
  .transition(height, 4s, 12s, ease-out);
}
.test-multitransitions {
  .transition(width, height, top; 1s, 2s; 0s, 1s, 3s; ease-in, ease-out, ease);
}
.test-not-compact {
  .transition(width, height, top; 1s, 2s; 0s, 1s, 3s; ease-in, ease-out, ease; false);
}
.test-raw-input {
  .transition(@raw-input: top 1s, bottom 1s, color 3s 1s linear;);
}

In the above examples, note two things in particular: (1) how the multiple values need to be passed using commas to separate the lists, but semicolons to separate the parameter groups. So to visualize, it is this:

  .transition(width, height, top; 1s, 2s; 0s, 1s, 3s; ease-in, ease-out, ease);
              |---Properties----|-Dur.--|---Delay---|---------Timing--------|
                                |       |           |
                          semicolons divide groups of parameters

(2) how the raw-input example needs an ending semicolon to have it consider the commas as list items:

  .transition(@raw-input: top 1s, bottom 1s, color 3s 1s linear;);
                                                               |
                                                    semicolon here needed

CSS Output of Examples

.test {
  -webkit-transition:  all 1s 0s ease;
  -moz-transition:  all 1s 0s ease;
  -ms-transition:  all 1s 0s ease;
  -o-transition:  all 1s 0s ease;
  transition:  all 1s 0s ease;
}
.test-props {
  -webkit-transition:  width 1s 0s ease;
  -moz-transition:  width 1s 0s ease;
  -ms-transition:  width 1s 0s ease;
  -o-transition:  width 1s 0s ease;
  transition:  width 1s 0s ease;
}
.test-duration {
  -webkit-transition:  all 3s 0s ease;
  -moz-transition:  all 3s 0s ease;
  -ms-transition:  all 3s 0s ease;
  -o-transition:  all 3s 0s ease;
  transition:  all 3s 0s ease;
}
.test-delay {
  -webkit-transition:  all 1s 10s ease;
  -moz-transition:  all 1s 10s ease;
  -ms-transition:  all 1s 10s ease;
  -o-transition:  all 1s 10s ease;
  transition:  all 1s 10s ease;
}
.test-timing {
  -webkit-transition:  all 1s 0s linear;
  -moz-transition:  all 1s 0s linear;
  -ms-transition:  all 1s 0s linear;
  -o-transition:  all 1s 0s linear;
  transition:  all 1s 0s linear;
}
.test-all {
  -webkit-transition:  height 4s 12s ease-out;
  -moz-transition:  height 4s 12s ease-out;
  -ms-transition:  height 4s 12s ease-out;
  -o-transition:  height 4s 12s ease-out;
  transition:  height 4s 12s ease-out;
}
.test-multitransitions {
  -webkit-transition:  width 1s 0s ease-in, height 2s 1s ease-out, top 2s 3s ease;
  -moz-transition:  width 1s 0s ease-in, height 2s 1s ease-out, top 2s 3s ease;
  -ms-transition:  width 1s 0s ease-in, height 2s 1s ease-out, top 2s 3s ease;
  -o-transition:  width 1s 0s ease-in, height 2s 1s ease-out, top 2s 3s ease;
  transition:  width 1s 0s ease-in, height 2s 1s ease-out, top 2s 3s ease;
}
.test-not-compact {
  -webkit-transition-property: width, height, top;
  -moz-transition-property: width, height, top;
  -ms-transition-property: width, height, top;
  -o-transition-property: width, height, top;
  transition-property: width, height, top;
  -webkit-transition-duration: 1s, 2s;
  -moz-transition-duration: 1s, 2s;
  -ms-transition-duration: 1s, 2s;
  -o-transition-duration: 1s, 2s;
  transition-duration: 1s, 2s;
  -webkit-transition-delay: 0s, 1s, 3s;
  -moz-transition-delay: 0s, 1s, 3s;
  -ms-transition-delay: 0s, 1s, 3s;
  -o-transition-delay: 0s, 1s, 3s;
  transition-delay: 0s, 1s, 3s;
  -webkit-transition-timing-function: ease-in, ease-out, ease;
  -moz-transition-timing-function: ease-in, ease-out, ease;
  -ms-transition-timing-function: ease-in, ease-out, ease;
  -o-transition-timing-function: ease-in, ease-out, ease;
  transition-timing-function: ease-in, ease-out, ease;
}    
.test-raw-input {
  -webkit-transition: top 1s, bottom 1s, color 3s 1s linear;
  -moz-transition: top 1s, bottom 1s, color 3s 1s linear;
  -ms-transition: top 1s, bottom 1s, color 3s 1s linear;
  -o-transition: top 1s, bottom 1s, color 3s 1s linear;
  transition: top 1s, bottom 1s, color 3s 1s linear;
}

If long form is never desired then the mixin code can reduce to this:

.transition (@props: all; 
             @duration:1s; 
             @delay: 0s; 
             @timing: ease; 
             @raw-input: false) {
  .output() when (@raw-input = false) {
    @propsLength: length(@props);
    @durationLength: length(@duration);
    @delayLength: length(@delay);
    @timingLength: length(@timing);
    .buildString(@i, @s: ~'') when (@i <= @propsLength) {
      @prop: extract(@props, @i);
      .setDuration() when (@i <= @durationLength) {
        @dur: extract(@duration, @i);
      }
      .setDuration() when (@i > @durationLength) {
        @dur: extract(@duration, @durationLength);
      }
      .setDuration();
      .setDelay() when (@i <= @delayLength) {
        @del: extract(@delay, @i);
      }
      .setDelay() when (@i > @delayLength) {
        @del: extract(@delay, @delayLength);
      }
      .setDelay();
      .setTiming() when (@i <= @timingLength) {
        @time: extract(@timing, @i);
      }
      .setTiming() when (@i > @timingLength) {
        @time: extract(@timing, @timingLength);
      }
      .setTiming();
      .setDivider() when (@i > 1) {
        @divider: ~'@{s},';
      }
      .setDivider() when (@i = 1) {
        @divider: ~'';
      }
      .setDivider();
      @string: @divider @prop @dur @del @time;
      .buildString((@i + 1), @string);  
    }
    .buildString(1);
    .buildString(@i, @s: ~'') when (@i > @propsLength) {
      .compact(@s);
    }
  }
  .output() when not (@raw-input = false) {
    .compact(@raw-input);
  }
  .compact(@string) {
    -webkit-transition:@string; 
       -moz-transition:@string;
        -ms-transition:@string;
         -o-transition:@string; 
            transition:@string;    
  }
  .output();
}
ScottS
  • 71,703
  • 13
  • 126
  • 146
  • This is a neat solution... (Please, don't take it as a critique, just a minor remark) But personally I would prefer to use some small distinct "[property merge](https://github.com/less/less-docs/blob/master/content/features/merge.md)" based mixins than one gigantic "I can do everything" routine. (Not counting that with a modern "no prefixes" approach (because of things like [autoprefixer](https://github.com/ai/autoprefixer)) this can be done with no mixins at all. I.e. just by using `transition+: ...;`, and mixins become useful only for some animation "templates" or so). – seven-phases-max Dec 28 '13 at 14:38
  • @seven-phases-max: I actually was working on another property merge solution when I encountered [this bug](https://github.com/less/less.js/issues/1742), which has [apparently been fixed (by you I believe)](https://github.com/less/less.js/pull/1743), but is still not working for me at less2css.org in the 1.5.1 release (so perhaps 1.5.2?). – ScottS Dec 28 '13 at 18:16
  • Ah right, I almost forgot about this bug. Yes, coming (I hope soon) 1.6.0 has it fixed. – seven-phases-max Dec 28 '13 at 18:42
5

From less.js 1.3 onwards you have to specify ... in the argument list to signify that more arguments can be added. e.g.

.transition-property(...) {
 foo: @arguments;
}
Luke Page
  • 8,136
  • 1
  • 20
  • 22