less / less.js

Less. The dynamic stylesheet language.
http://lesscss.org
Apache License 2.0
17.02k stars 3.41k forks source link

How to prefix keyframes #1264

Closed jimisaacs closed 10 years ago

jimisaacs commented 11 years ago

I see a lot of issues about having support for @keyframes, but I wanted to figure something out.

How in the world can I use LESS to simplify having to put multiple keyframe blocks for each browser "@-moz-keyframe", "@-webkit-keyframes", etc.? Is it possible, or even worth trying?

I've looked all over, and tried myself. I see some pretty complicated, and unusable solutions in my case. Basically I am wondering if we are required to copy and paste keyframes and prefix them, am I winning anything by using LESS for this type of CSS? The types of things I (and many other devs) build now days have many and many different kinds of animations per project.

I'm am at a loss here. Even if it is actually possible to use some sort of complicated mixin, still using a nested prefixed mixin inside a keyframe is kind of pointless, it would output something like:

@keyframes myAni {
    100% { 
        -webkit-transform: translate(100px, 0);
        -moz-transform: translate(100px, 0);
        transform: translate(100px, 0);
    }
}
@-webkit-keyframes myAni {
    100% { 
        -webkit-transform: translate(100px, 0);
        -moz-transform: translate(100px, 0);
        transform: translate(100px, 0);
    }
}
@-moz-keyframes myAni {
    100% { 
        -webkit-transform: translate(100px, 0);
        -moz-transform: translate(100px, 0);
        transform: translate(100px, 0);
    }
}
lukeapage commented 11 years ago

I can't think of a way :(

another reason to implement this https://github.com/cloudhead/less.js/issues/1199

jonschlinkert commented 11 years ago

:+1:

jonschlinkert commented 11 years ago

Rather than trying to create a dynamic syntax to solve this isolated problem, my suggestion is that Less.js automatically expand keyframes rules to include vendor-prefixed versions. So if a dev did this:

@keyframes slidein {
  0%, 50%, 100% {opacity: 0; }
  25%, 75% {opacity: 0; }
}

Less.js would automatically generate the vendor-prefixed versions in the CSS.

I am opposed to "auto-prefixing" for properties, but for keyframes it makes sense. This alone would cut down a ton of code in .less files, so it would be a big win for devs.

chriseppstein commented 11 years ago

This is the use case that caused Sass to introduce passing content to mixins. It enabled compass to support animations instead of putting that logic in sass.

jonschlinkert commented 11 years ago

@chriseppstein thanks. to clarify though, content is being passed specifically for the keyframe lists (e.g. 0%, 50%, 100% {opacity: 0; }), correct? Beyond the keyframe lists, SASS also has methods for dealing with the vendor prefixes and identifiers, and since all of the major challenges with keyframes have been addressed, it seems that passing in content is more advantageous within that construct. Or am I missing a crucial point here?

Whatever approach Less.js ends up taking to address keyframes it should result in a transformative net benefit to developers. IMHO, passing content into mixins is only a part of the equation, and unless we also address vendor-prefixed at rules, keyframe identifiers, and keyframe animations, then passing in content will only result in an incremental benefit.

To your point about adding logic, it seems like there is going to be some kind of trade-off here regardless of how we choose to solve the problem. Any advice on which problem is the most important to solve here?

chriseppstein commented 11 years ago

@jonschlinkert It's true. Passing content was useful because of the way the suite of features in Sass interact with each other. And beyond keyframes, we saw numerous ways that passing content blocks provided a general expressiveness to create abstractions that defined "containers" instead of the contents of those containers. As such, you can use mixins with content blocks to abstract selector hacks, media queries, css modules, etc.

Obviously Less will have to find it's own complementary concepts that mesh as seemlessly as possible with the existing feature set.

I'm not sure I understand your question about trade-offs. Our design goal for Sass is that it won't need a new release just because a browser adds a feature. We offload the knowledge of how what browsers capabilities are and how interop should work to frameworks built on Sass, because there is not "one true way" to address that problem and we want to allow room for frameworks to innovate in that space.

matthew-dean commented 11 years ago

The thing about passing content is that the use cases seem to be narrower. That is, rather than a generic catch-all construct that requires you to define mixins for each type of @ block and insert a @content block within that mixin, I'm wondering if there's a way for us to redefine properties and keywords so that they "auto-expand". But WITHOUT having to make Less manage those prefix extensions (especially since this has a high maintainability cost).

One of the disappointing things to me about LESS and SASS is that CSS needs to be rewritten in a different syntax construct for the same (type of) output. (That is, that background-size: value needs to be rewritten as .background-size(@value))

Like @jonschlinkert, I would like to write this:

@keyframes slidein {
  0%, 50%, 100% {opacity: 0; }
  25%, 75% {opacity: 0; }
}

...and have it just work (give me the prefixes I want). How do we achieve that? One option is that the code is auto-rewritten by the parser, but I think @chriseppstein's observation is correct: that this is a self-limiting approach.

However, I think there's another option that hasn't been explored very much, which I started to explore in #1342: that we allow the author to "redefine" keywords and properties, and assign them to mixins. Such that needing that output doesn't require rewriting your CSS3 code, and at such time when browsers had matured beyond needing prefixes, that you coud remove the mixin, and all of your code would remain valid. I think it's a powerful pattern for extending CSS authorship in a way that can be more intuitive than mixin calls.

So, for example, say I could write something like this code:

@define @keyframes() {
  @-webkit-keyframes @params @block;  //Just spitballing that what follows the keyword up to the first brace could be assigned to a var, and the braced value assigned to another var, but not sure it's needed
  @keyframes @params @block;
}

animation(@value) { 
  animation: @value;
  -webkit-animation: @value;
}

Then, I write absolutely straightforward CSS3. No special mixin syntax in sight:

@keyframes OPACITY_FADE {
  0%   { opacity: 0.25; }
  25% { opacity: 1; }
  100% { opacity: 0.25; }
}
.box {
  animation: OPACITY_FADE 1s infinite;  
}

// Outputs:

@-webkit-keyframes OPACITY_FADE {
  0%   { opacity: 0.25; }
  25% { opacity: 1; }
  100% { opacity: 0.25; }
}
@keyframes OPACITY_FADE {
  0%   { opacity: 0.25; }
  25% { opacity: 1; }
  100% { opacity: 0.25; }
}
.box {
  animation: OPACITY_FADE 1s infinite;  
  -webkit-animation: OPACITY_FADE 1s infinite;  
}

Like #1342, the values I use in my CSS are passed to my defined mixins. Technically, I could use this construct to create all new keywords, properties, and block containers, but I can also powerfully use this to gracefully support properties, the way we use unobtrusive JavaScript. It's unobtrusive LESS. And it requires no special maintenance of which properties need prefixes.

I think this could be a really powerful pattern, and people would get excited to a) use the CSS they already know, b) write CSS-like syntax to create complex output. I would liken it to the Web Components work that's happening in HTML5, where I can define complex markup, but then call it with a single simplified element the same way I write any other element.

I don't know about you @jonschlinkert, but I'm really excited about the idea of component-based architecture. ;-)

Anyway, that's my $0.02.

jimisaacs commented 11 years ago

@matthew-dean The part that I'm still confused about is how this would work for prefixed styles within the keyframe rules. Do you have an example for an animation using transform, -webkit-transform, etc. ?

matthew-dean commented 11 years ago

@jimisaacs Absolutely. It would look something like this:

transform(@value) {
    -webkit-transform: @value;
    -moz-transform: @value;
    -ms-transform: @value;
    -o-transform: @value;
    transform: @value;
}
.rotate {
  transform: rotate(45deg);
}

The "value" is assigned to a single-variable mixin. If an author is worried about conflicts, or unintended consequences of overriding properties, they could also do this:

-prefix-transform(@value) {
    -webkit-transform: @value;
    -moz-transform: @value;
    -ms-transform: @value;
    -o-transform: @value;
    transform: @value;
}
.rotate {
  -prefix-transform: rotate(45deg);
}

The syntax / usage for mixins as properties seems clearer than the @keyframes example, so I think the @keyframes syntax would need some refinement. You should direct other questions about mixin property syntax to Issue #1342. I referenced it here because it makes sense to apply a similar concept in redefining keywords so that they can "auto-expand", but at the total control of the LESS author.

chriseppstein commented 11 years ago

One of the disappointing things to me about SASS is that CSS needs to be rewritten in a different syntax construct for the same (type of) output.

This is a Sass feature. It makes it easier to understand what's happening. Transparent behaviors that override existing syntax, create debugging complexity and developer confusion. The problem here is that the syntax is so different from the property itself. So I want to bring something like this to sass:

.cover {
  +background-size: cover
}

Which would be syntactic sugar for @include background-size(cover).

Unfortunately + creates some syntactic ambiguities so we'll probably have to pick a different syntax.

matthew-dean commented 11 years ago

Sorry sorry, now I get your question. I missed the bit about transforms within keyframes syntax. Hmm... working through it, that may be a more complex issue, since we'd want to change at least variables on the content.

@define @keyframes() {
  @-webkit-keyframes @params {
    @prefix-webkit: true;
    @content;  // looks more unavoidable at this point
  }
  @keyframes @params {
    @prefix-webkit: false;
    @content;
  }
}

// ...etc with mixins that have guards to add prefixes based on the value of @prefix-webkit
matthew-dean commented 11 years ago

Transparent behaviors that override existing syntax, create debugging complexity and developer confusion.

Even for the developers that created the overrides? o_O

I don't see how it's any more confusing than running something like -prefix-free, except that you've DEFINED the prefixing behaviors, so you should have MORE awareness of what's happening in your code. Unless you downloaded a mixin library which did this for you, in which case it has the exact same result as running -prefix-free., only you can more easily modify the rules that modify those prefixes. So, I don't see how that argument makes sense. Nothing would "magically happen" without your knowledge or without you at least setting up a library, which should document exactly what will happen.

LESS already converts simple code declarations into more complex ones; this is just a method that allows you to use a syntax you are familiar with, as you pointed out.

chriseppstein commented 11 years ago

Even for the developers that created the overrides? o_O

yes. even for the developer that created the overrides. Or the developer who takes over your project, or the one you just added to your team. Sass is built for growth and maintainability over long time frames. How you built the project you worked on two years ago isn't always fresh in your mind.

I don't see how it's any more confusing than running something like -prefix-free

I didn't claim that it was less confusing than prefix-free -- I have the same critique of that project.

you should have MORE awareness of what's happening in your code.

You should. But maybe it's imported and maybe two years have past.

Unless you downloaded a mixin library which did this for you, in which case it has the exact same result as running -prefix-free.

I build such a mixin library. And we don't have the same problem as prefix-free because we keep our syntax distinct.

So, I don't see how that argument makes sense.

It's true, the strawman argument you're using doesn't make much sense.

Nothing would "magically happen" without your knowledge or without you at least setting up a library, which should document exactly what will happen.

Knowledge is often an ephemeral thing. Reading docs is not most people's first step. I always advocate for clarity for the reader, at the low cost of an extra few characters for the writer. Anyways, that's our philosophy for Sass, if Less wants to be magical like stylus then by all means, go ahead. I'm of the opinion that a little less magic and a little more syntax is good for developers, but reasonable people can disagree about what experience they want to optimize.

matthew-dean commented 11 years ago

Easy, friend, I think we agree on some important aspects. For instance, this syntax is close to my proposal:

.cover {
  +background-size: cover
}

Considering the dash starts a prefix now, it might make more sense to do this:

.cover {
  -background-size: cover;
}
-background-size(@values) {
  -webkit-background-size: @values;
  -moz-background-size: @values;
  background-size: @values;
}

Also, my only use case for mixin properties without dashes or dots was not just for property overrides. Yes, I considered that it has some potential for abuse. But that's not the only reason why defining custom properties might be valuable. And (while we're on the subject of strawmen), if an author is concerned about what they'll forget two years from now, and not be able to understand what they've done, they can define custom properties just as you've said, with a custom mixin name, or a dash syntax, or the traditional dot syntax. The point is simply that the developer can define the custom property.

However, the great irony in all of this is that a lot of these solutions seem to apply to prefixing, and we're nearing a time when: a) many vendors are supporting unprefixed properties, b) browser vendors and W3C are working to transparently solve the problem of prefixes. (http://www.sitepoint.com/css3-vendor-prefix-crisis-solutions-2/)

For keyframe animations, -webkit is the only other value required, which is not much duplication. For backgrounds and borders (like background-size), no prefixing is needed any longer (http://caniuse.com/#feat=background-img-opts). It doesn't mean it's not worth exploring, but any new feature added just (or mainly) to support prefixing does potentially have a reduction of value over time.

jimisaacs commented 11 years ago

@matthew-dean Yeah exactly, I almost replied without making it to the bottom and seeing your 2nd reply ;) I'm really sick and tired of browser prefixes, just dealing with them period, which is why I can understand why keeping them out of LESS is preferred (it's not really a problem LESS should solve). It is just a little disappointing that using LESS really gives minimal benefits to the point of this issue. It would be nice to have a local tool to do the job of http://prefixr.com/, because I actually prefer keeping it a post compile pass. Whether or not it should be part of LESS is another story :/ Maybe just a sister project.

Something like: compile LESS to non prefixed css -> prefix pass -> optionally optimize

matthew-dean commented 11 years ago

@jimisaacs EVERYONE is sick of browser prefixes. ;-)

jimisaacs commented 11 years ago

@matthew-dean My evolution as a web developer:

Sorry off topic, I should probably just close this issue if I don't even believe it should be addressed anymore ...

matthew-dean commented 11 years ago

@jimisaacs Yeah, not sure. Prefixes in properties we can solve with mixins and variables in property names. @keyframes is a bit more complex, and maybe we can't or shouldn't really solve them in the same way.

matthew-dean commented 11 years ago

Like @chriseppstein mentioned, probably the most generic flexible approach would be being able to send a group of selectors to a mixin through a variable, which is being currently discussed in separate issues.

It does seem like so far the use cases for doing so are supporting prefixes for @keyframes and grouping media queries, but it is a compelling point that other reasons may exist in the future.

jonschlinkert commented 11 years ago

https://github.com/cloudhead/less.js/issues/1348

jonschlinkert commented 11 years ago

probably the most generic flexible approach would be being able to send a group of selectors to a mixin through a variable

I'm not so sure. My money is on:

Of those, interpolated mixins and interpolated properties would definitely have the greatest impact since they are not specific to keyframes and have so many uses.

Here are some examples of what could be accomplished with the above.

Merging keyframes lists and rules

First, let's create some super basic keyframes list mixins:

.fadein() {
  0%, 50%, 100% { .opacity(0); } 
  25%, 75%      { .opacity(1); }
}
.fadeout() {
  0%, 50%, 100% { .opacity(1); } 
  25%, 75%      { .opacity(0); }
}
.change-color() {
  0%, 50%, 100% { color: blue; }
  25%, 75%      { color: red; }
}

Aside from CSS animations, these keyframes lists contain most of the "magic" in keyframes. But they also present the greatest challenge in terms of making keyframes easier to work with, and more maintainable over time.

So let's say you get the urge to abstract out a bunch of "utility mixins" for keyframes rules, regardless of whether or not that would be useful, Less.js currently does not merge rules in keyframes lists. To use a pragmatic example, if you wanted to use both the .change-color() mixin and the .fadeout() mixin together inside the same keyframes declaration block(s), like this:

@-webkit-keyframes fadein { .fadein(); .change-color(); }
   @-moz-keyframes fadein { .fadein(); .change-color(); }
     @-o-keyframes fadein { .fadein(); .change-color(); }
        @keyframes fadein { .fadein(); .change-color(); }

the output would be something like this (for each vendor):

@-webkit-keyframes morph {
  0%, 50%, 100% { opacity: 0; }
  25%, 75%      { opacity: 1; }
  0%, 50%, 100% { color: blue; }
  25%, 75%      { color: red; }
}

but what we want is this:

@-webkit-keyframes morph {
  0%, 50%, 100% { opacity: 0; color: blue; }
  25%, 75%      { opacity: 1; color: red; }
}

So that's the first thing that would make keyframes easier to manage.

Interpolating keyframes identifiers

This one is easier to illustrate. Currently, this works:

``` css
// This works
.keyframes-fadein(@identifier: fadein) {
  @-webkit-keyframes fadein { .fadein(); }
     @-moz-keyframes fadein { .fadein(); }
       @-o-keyframes fadein { .fadein(); }
          @keyframes fadein { .fadein(); }
  .@{identifier} {
    -webkit-animation-name: @identifier;
       -moz-animation-name: @identifier;
         -o-animation-name: @identifier;
            animation-name: @identifier;
  }
}
// Call the mixin
.keyframes-fadein();

But this does not:

.keyframes-fadein(@identifier: fadein) {
  @-webkit-keyframes @identifier { .fadein(); }
     @-moz-keyframes @identifier { .fadein(); }
       @-o-keyframes @identifier { .fadein(); }
          @keyframes @identifier { .fadein(); }
}

But even if it did, by itself it would only make things just a tiny bit easier to manage, and we would still have to add the .fadein(); mixin to each declaration. So we're only making incremental impact so far.

Interpolation for mixin names

This is also pretty simple to illustrate:

// With simple animations, the mixin name 
// might be the same as the identifier name
.keyframes(@identifier: fadein, @mixin: @identifier) {
  @-webkit-keyframes @identifier { .{mixin}(); }
     @-moz-keyframes @identifier { .{mixin}(); }
       @-o-keyframes @identifier { .{mixin}(); }
          @keyframes @identifier { .{mixin}(); }
  .@{identifier} {
    -webkit-animation-name: @identifier;
       -moz-animation-name: @identifier;
         -o-animation-name: @identifier;
            animation-name: @identifier;
  }
}

Assuming that we have a library of "keyframes list mixins" somewhere, and we also have a library of CSS animations mixins (which for the most part can already be addressed by existing Less.js features), then for simple animations we will be able to do this:

.keyframes(fadein);
.keyframes(fadeout);
.keyframes(shake);

and even better, this:

#keyframes > .fadein(1.5s);
#keyframes > .fadein(2s);
#keyframes > .fadein(2s);
#keyframes > .move(1, 2, 3);
...

Interpolated properties should speak for themselves at this point. But to give you an idea of how varied keyframes animations are when all of the vendor prefixes are accounted for, I encourage you to take a look at [animate.css](https://github.com/daneden/animate.css/blob/master/animate.css

jonschlinkert commented 11 years ago

This hack just illustrates some of the challenges with making keyframes easier to manage. There are a bunch of different ways to do this with keyframes, but here is one that's fairly straightforward:

.keyframes(@identifier: slidein, @animation: ~"{0%, 50%, 100% { opacity: 0; } 25%, 75% {opacity: 0 }") {
  .webkit { content: ~"''; } @-webkit-keyframes @{identifier} @{animation}"}
     .moz { content: ~"''; }    @-moz-keyframes @{identifier} @{animation}"}
      .ms { content: ~"''; }     @-ms-keyframes @{identifier} @{animation}"}
       .o { content: ~"''; }      @-o-keyframes @{identifier} @{animation}"}
     .key { content: ~"''; }         @keyframes @{identifier} @{animation}"}
}
.keyframes(slidein, ~"{0%, 50%, 100% { opacity: 0; } 25%, 75% {opacity: 0 }");
.keyframes(progress-bar-stripes, ~"{from {margin-left: 100%; width: 300% } to {margin-left: 0%; width: 100%; }");
.keyframes(flash);

To force it to output valid CSS, there are some useless vendor-prefixed classes with the randomly chosen content rule. Paste it into less2css.org and you'll see what I mean. It's a quick way of understanding why keyframes are hairy.

matthew-dean commented 11 years ago

Good examples. But... just to reiterate this point, many of us are using or referencing code examples that are already out of date.

For CSS animation, you only need unprefixed and the -webkit- prefix. That's it. Unless for some reason you're targeting > 99.5% of users. http://caniuse.com/#search=keyframes

Habitually, many people are taught to append all the prefixes for all the CSS3 things, but that hasn't been true for years. Even writing both -webkit- and unprefixed is somewhat annoying, but I just want to make sure we're correctly identifying the scope of the issue. Many properties need very few prefixes any more, and some properties that people are prefixing don't need prefixing at all.

@jonschlinkert - I think you hit onto something I wondered about, which is with all the variable interpolation we're planning, plus mixin guards, we might be able to abstract keyword syntax already, but I just wasn't sure if it was possible, without working through all the mixins required.

kuus commented 10 years ago

Hi, I already put the link in another related topic, anyway this is a mixin I wrote for less animations and keyframes https://github.com/kuus/animate-me.less

thybzi commented 10 years ago

Also, check out my solution for the same problem: https://github.com/thybzi/keyframes/

Soviut commented 10 years ago

I just use Autoprefixer in my Grunt build. I don't have to even think about vendor prefixes anymore thanks to it: https://github.com/ai/autoprefixer (the grunt plugin https://github.com/nDmitry/grunt-autoprefixer)

It adds prefixes based on stats from caniuse.com so it only adds relevant prefixes based on the level of compatibility you set. It also strips out unnecessary/obsolete prefixes from bloated stylesheets.

I realize this may not be a solution for everyone as there may be specific reasons to need to manually build in the prefixes. But I get the impression that much of the prefix discussion around LESS is based on people simply not knowing tools like Grunt and Autoprefixer exist.

pixelass commented 10 years ago

HM.. So for now..

This is how I did it: (Thanks to the idea from @jonschlinkert ) https://github.com/pixelass/more-or-less/blob/master/less/css3/animation/_keyframes.less

Explanation: https://github.com/pixelass/more-or-less#keyframes

Looking at it almost makes me vomit .. but except from the ugly syntax and output it works

matthew-dean commented 10 years ago

It's unfortunate if JavaScript hacks within LESS are the only current solution. However, there's some progress being made for several of these points: passing mixins via a variable and variable interpolation for keyframes.

pixelass commented 10 years ago

at least I was able to remove all '~' in my library. Some were not needed, others could be replaced by e('foo-@{bar}'). So a "find-all ~" inside my library returns: "No results found".

UPDATE. Strangely all my tests failed today so I had to revert the changes to removing the '~' in vendor prefixes. (updated to less 1.6.2 yesteray)

...which doesn't really change anything... except now my mixin looks like this:.

.keyframes(@name; @frames...) {
    @vendorPrefixes: -webkit-, -moz-, '';
    .for(@vendorPrefixes);.-each(@i) {
        - {-:e("-} @@{i}keyframes @{name} {@{frames}")}
    }
}
seven-phases-max commented 10 years ago

Closing as solved with #1860.