Colin Bacon, web developer.

Prevent transitionend event firing twice

Prevent transitionend event firing twice

I came across this problem when handling a CSS transitionend event. The transitionend event fires when a CSS transition has completed. The problem wasn't that it wasn't firing, it was firing, but twice.

Here is an example of my event handler.

$('.bacon').one('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend', function() {
    console.log('Transition complete');
});

As you can see I am using jQuery .one(). Unlike .on() this handler is only executed once, so the fact that it was firing twice was really confusing. I have used this before in previous projects and not had this problem.

The answer

The clue was in the description from jQuery docs:

The handler is executed at most once per element per event type.

Once per element per event type. Logging the event type to the console makes it clear what is happening.

$('.bacon').one('webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend', function(e){
    console.log(e.type);
});

This is what was output in the console of Chrome dev tools.

transitionend
webkitTransitionEnd

Since Chrome 36, both webkitTransitionEnd and transitionend have been supported. Meaning that correctly, the event handler was firing once per event type.

The fix

To resolve this we need to test which event name the browser supports.

Modernizr has a method called Prefixed which detects the CSS feature you pass into it and returns either the prefixed or nonprefixed property name. The docs use the following as an example.

var transEndEventNames = {
    'WebkitTransition' : 'webkitTransitionEnd',// Saf 6, Android Browser
    'MozTransition'    : 'transitionend',      // only for FF < 15
    'transition'       : 'transitionend'       // IE10, Opera, Chrome, FF 15+, Saf 7+
},
transEndEventName = transEndEventNames[ Modernizr.prefixed('transition') ];

$('.bacon').one(transEndEventName, function() { 
    console.log('Transition complete');
});

Or without Modernizr (taken from David Walsh Blog):

function whichTransitionEvent() {
    var el = document.createElement('fake'),
        transEndEventNames = {
            'WebkitTransition' : 'webkitTransitionEnd',// Saf 6, Android Browser
            'MozTransition'    : 'transitionend',      // only for FF < 15
            'transition'       : 'transitionend'       // IE10, Opera, Chrome, FF 15+, Saf 7+
        };

    for(var t in transEndEventNames){
        if( el.style[t] !== undefined ){
            return transEndEventNames[t];
        }
    }
}

var transEndEventName = whichTransitionEvent();

$('.bacon').one(transEndEventName, function() {
    console.log('Transition complete');
});

Either methods ensure that the transitionend event is only fired once.

Note: From Firefox 49 onwards a number of webkit prefixed properties are supported (see Firefox 49 for developers compatibility section). This means Firefox supports webkitTransitionEnd as well as transitionend and either will work. Thanks to Jarno de Haan for pointing this out.

Summary

We often deal with vendor prefixes in CSS, aware that one day they will be replaced once implemented in the formal spec. But with no breaking changes to our CSS. However that is not the case with transition end events, and our once working code can behave in unexpected ways. The take away from this is it is wise to always detect for an event, property or feature instead of a capture all approach. This will ensure our code is future proof and stable.