Transition events (and transforms) community expectations research

It’s been some months since Rossen requested this research on transition events and transforms back at our Seattle face to face at the start of the year. Seems it’s been sitting in my outbox all this time. So without further ado!



TLDR

This started as an investigation into transforms and transitions behavior, but quickly swelled to encompass expected transition events across all CSS properties.



  *   The general developing public expects transition events only to fire if a visual change has actually occurred. Sensible, but….
  *   …library developers expect reliable behavior, something they know will fire. The sensibility of the above is trampled by library authors’ need for reliability.
  *   Browsers seem uniformly arbitrary about which computed values they fire transitionend on--with at least one deviation from Edge.
  *   Few folks knew about transitionstart/run/cancel, even though they might've solved some problems on projects like Angular. Surprisingly, Chrome lags behind other browsers by not adopting them.



Caveats

This conversation originally started around transitionend, but since the original Transitions spec<https://drafts.csswg.org/css-transitions/#event-transitionevent>, transitionrun, transitionstart, and transitioncancel have been added and are supported by FireFox and Edge (except for transitionrun). Some of the developers I interviewed expressed what sounded like a yearning for events like these, but weren't aware of their existence.



When discussing transition events in the context of this report, we are specifically discussing transitionend.



Background/Refresher

At the Seattle face to face, there was a discussion<https://log.csswg.org/irc.w3.org/fx/2017-01-11/> of whether or not transition events should fire when a transform's  value changes but computed value remains the same (like translateX(0) and translate3d(0, 0, 0)); the transforms spec<http://dev.w3.org/csswg/css-transforms/#interpolation-of-transforms> treats them both as translate3d(0, 0, 0). But transforms are the only properties that this behavior is described for. What about other properties like color, filters, widths that could have different start and endpoint values that visually are identical/calculate to the same value?



Blink currently fires a transition in this case, even though visually nothing has changed. The group decided to continue that behavior for now: "transitions will run whenever the functions differ, even if the endpoints are functionally identical," with the possibility of changing over to interpolating both values and comparing them to determine if a transition should trigger after an investigaton. Well the results are in! And they might surprise you.



Interop

Surma created a few test cases<http://f.surma.link/tmp/transitiontest/> that revealed some unexpected behaviors<http://f.surma.link/tmp/transitiontest/comparesheet.pdf>: Browsers seem uniformly arbitrary about which computed values they fire transitionend on! I suspect there're some rendering commonalities resulting in parallel behavior evolution here. Transitionenddoesn't fire for most of these property tests Surma made, just transform and (in some cases not ours) width.



To that end, we do one thing differently from other browsers in these tests: we don't fire transitionend events on width changes like 50vw -> 50%!



Outreach

I reached out on Twitter<https://twitter.com/rachelnabors/status/828668624319586304> and on the Animation at Work Slack<http://slack.animationatwork.com/>, to gauge what everyday developers thought. The results were split, with more than 50% of respondents saying that if the computed value doesn't change, no transition event should fire.



But no one came forth with any case studies, probably because most developers, even animation-using ones, will rarely touch a transitionendevent more than once or twice on a project. transitionend is more useful to folks building UI infrastructure, listening for when they should remove an element from the DOM or chaining animations.



So next I decided to hit up just the folks who work on libraries. These libraries must to rely on animationend and transitionend events to chain animations. If anyone should have an opinion, it should be these devs.



I approached the folks at GreenSock, Bodymovin' (a popular export library for AfterEffects that Facebooks Lottie is built on), Mo.js, ReactMotion, Angular, Ionic, Anime.js, React and the new Vue.js.



Using the test cases provided by Surma, I kicked conversations off with these developers by asking, "Do you expect transitionend to fire on these property changes?



http://output.jsbin.com/yorakom/8


http://output.jsbin.com/dubexe/7


http://output.jsbin.com/vevomon/1




And do you do anything to anticipate or work around their current behavior?"



Their responses were interesting:



Library


Library uses transitionend?


Author expects…


Author would like…


Author/ Contact


Mo.js


No


Fires always


Fires always


Oleg Solomka


Greensock


No


Fires always


Fires always


Jack Doyle


Angular(JS / animations)


Yes


Never fires


Fires always


Matias Niemelä


Ionic


Yes


Fires only on visual change


Fires always


Adam Bradley


Anime.js


No


??


??


Julian Garnier


React-Motion


No


Fires always


Always fires unless value is "exactly, textually the same."


Cheng Lou


ReactCSSTransitionGroup


Yes


Sometimes fires


Fires Always


Ben Alpert


Vue.js


Yes


Fires only on visual change


Fires


Chris Fritz, Evan You


Bodymovin'


No


Fires only on visual change


Fires only on visual change


Hernan Torrisi




Detailed feedback

Most authors were ambivalent about whether the event should fire if the values change but the visuals don’t, but most expressed a preference for consistency above all else.



Cheng Lou of React-Motion said '…it's better keeping it stupid, and fire the event. The declarative, "not firing event" option might be a nice default in certain situations, but it's much easier on the JS side to manually prevent events from happening, than needing to undo the clever default behavior of "not firing event" in certain situations by firing the event ourselves.



'…We don't use css events in react-motion; things are driven in JS. The tradeoff is that we get this kind of control and the user (theoretically; not implemented yet) get to decide whether the equivalent "transitionend" event happen or not based on a custom comparator. At best, I think transitionend not triggering makes sense when the value is exactly, textually the same. Intelligent, baked-in equality of css values would probably fire back?



'That's my opinion anyway. React-motion does things in pure JS (would love to take advantage of CSS' performance though...) partially because this gives us enough control and realistically we don't end up debugging cross-browser issues (I actually recall safari's transition events being buggy). I can imagine other browsers detecting the equivalence of values differently or something.'



This jives with what I've heard from others. The author of anime.js chose to work with JavaScript only after finding CSS transition events too unreliable:



"I always had mixed results using transitionend, it wasn't super reliable from what I remember. That's one of the reason that motivated me to make a JS animation library with a more reliable 'complete' callback :) "



Matias Niemela who works on Angular's animation features at Google says, "It doesn't fire if a style changes, but no visual effect… This problem makes using transitions within a framework totally horrible because there's no way to validate if the element actually kicked off an animation." Also "In some browsers, if you animate something like border then it will fire four events (top, left, right, bottom). So having a framework understand and operate on this glitch is annoying."



Oleg Solomka, author of popular library Mo.js (which doesn't rely on transition events) says, "I understand that css transitions are sloppy thus do expect the event to fire."



Jack Doyle from GreenSock strongly supports a "fires regardless" model and demonstrated how devs would have to check if start and end values match:



//GOOD (assumes we can trust transitionend to fire):

element.addEventListener(“transitionend”, doTheNextThing);

element.style.width = newValue;



//BAD (only fires if start/end values don’t match, so we gotta check):

if (document.defaultView.getComputedStyle(element).width === convertToPixels(newValue)) {

    setTimout(doTheNextThing, convertToMilliseconds(document.defaultView.getComputedStyle(element).transitionDuration));

} else {

    element.addEventListener(“transitionend”, doTheNextThing);

    element.style.width = newValue;

}



'The only down side (that I can think of) to firing it when the (computed) values match is that it might be interpreted as signaling [a visual change] (even when there wasn’t). Practically speaking, though, I don’t really anticipate that causing many real-world problems for people. “Dangit! The transitionend fired even though there was no change…my app broke!” (Doubt it)'



Ionic's Adam Bradley said, "I guess I'd expect every scenario to either not fire, or they would all fire, but not half and half. Like when the resulting value is still identical, even though it uses a different unit, I'd expect it not to fire the transition. That said, if there was a good reason for them to always fire I'd be ok with that, just as long as it's a standard for all properties and not just some cases."



Ben Alpert who has worked on React's CSSTransitionGroup says, "Ultimately we gave up and switched to a timer-based approach where you manually specify the time in ms when using the component, and we ignore the CSS events if that's present." (Using timers is a major no-no, as it removes control of animation timelines from the browser, makes chaining more fragile.) "I like transitionend to fire for all of those cases because it's pretty confusing when degenerate cases have different behavior than all non-degenerate cases… I would have liked some event that is guaranteed to fire when a certain transition is done (and probably one that fires immediately if no transition happens) -- though I understand that there's currently no way (AFAIK) to identify a specific transition from JS. Even a way to globally wait for all transitions on an element (if any) to finish would be helpful and the closest thing to our original needs."



Lastly, Evan You of Vue.js claimed to never have run into any edge cases, but voiced the most divergent opinion:



"…my take on this is that:

  *   it should not fire if the two properties are guaranteed to result in the same underlying value in all cases;
  *   it should fire if they *can* result in different values, even if they happen to yield the same value in some cases."



And there you have it folks. The community seems too puzzled by transition events to rely on them, leading to a reliance on timers in some cases.



Thanks to Surma for putting together the tests and demos which greatly helped explain to folks where words failed!





Rachel Nabors

Received on Wednesday, 28 June 2017 16:34:23 UTC