219

I'd like to fade out an element (transitioning its opacity to 0) and then when finished remove the element from the DOM.

In jQuery this is straight forward since you can specify the "Remove" to happen after an animation completes. But if I wish to animate using CSS3 transitions is there anyway to know when the transition/animation has completed?

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
C.J.
  • 6,789
  • 7
  • 36
  • 45

6 Answers6

348

For transitions you can use the following to detect the end of a transition via jQuery:

$("#someSelector").bind("transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd", function(){ ... });

Mozilla has an excellent reference:

https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions#Detecting_the_start_and_completion_of_a_transition

For animations it's very similar:

$("#someSelector").bind("animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd", function(){ ... });

Note that you can pass all of the browser prefixed event strings into the bind() method simultaneously to support the event firing on all browsers that support it.

Update:

Per the comment left by Duck: you use jQuery's .one() method to ensure the handler only fires once. For example:

$("#someSelector").one("transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd", function(){ ... });

$("#someSelector").one("animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd", function(){ ... });

Update 2:

jQuery bind() method has been deprecated, and on() method is preferred as of jQuery 1.7. bind()

You can also use off() method on the callback function to ensure it will be fired only once. Here is an example which is equivalent to using one() method:

$("#someSelector")
.on("animationend webkitAnimationEnd oAnimationEnd MSAnimationEnd",
 function(e){
    // do something here
    $(this).off(e);
 });

References:

jwhitlock
  • 4,572
  • 4
  • 39
  • 49
Jim Jeffers
  • 17,572
  • 4
  • 41
  • 49
  • 15
    It's worth noting that the callback will be fired for *every* child element that transits as well. This is very important to keep in mind in case you are wondering why your callback gets fired more times than you were expecting. I am not aware of any work-around as of now. – SikoSoft Dec 16 '13 at 20:25
  • 24
    @Lev you could easily mitigate this by comparing the event's currentTarget to its target. So something like: function(event){ if(event.target === event.currentTarget){ /* Do stuff */ } } – Jim Jeffers Dec 20 '13 at 04:12
  • 5
    Yeah, I figured that out shortly after I wrote the comment. >_< Thanks for posting it though; I'm sure it will help others! :) – SikoSoft Dec 20 '13 at 09:36
  • 9
    We use `.on()` rather than `.bind()` for jQuery v1.7+ http://api.jquery.com/bind/ – olo May 21 '14 at 07:19
  • 3
    @JimJeffers you can use the "return home early" pattern, by doing, if( event.target != event.currentTarget ) return; – kroe Jun 03 '14 at 11:15
  • 3
    Use `.one()` instead of `.on()` or `.bind()`. This will ensure that it may only run once, and afterwards it'll unbind itself from the event handler. – ditto Jan 15 '15 at 18:20
  • Because I ran into a similar problem, i found the answer elsewhere however this SO answer was in my open tabs (just found answer on another tab first) anyways - here is a simple jsfiddle example of @AJFarmer use of .one() on a CSS `animation` transition [Jsfiddle](http://jsfiddle.net/qg72kvfj/) – DMSJax Dec 14 '15 at 16:00
  • 4
    All modern browsers now support the unprefixed event. http://caniuse.com/#feat=css-transitions Also note that if you have "transitionend webkitTransitionEnd" it will be triggered twice in Chrome. – WebDevNerdStuff Jan 01 '16 at 22:52
  • I prefer `one()` because you don't have to construct another jQuery object with `$(this)` just to remove the binding in the event handler. It just seems cleaner. – Xharlie Apr 01 '16 at 09:47
  • Since jQuery is in the picture, anyway, I have a simple and robust solution to ensure that a callback only gets fired once: the code that _begins_ the transition returns a _promise_ that is resolved by the `animationend` handler. The code that is to run after the animation ends is simply queued on the promise with `then()` – Xharlie Apr 01 '16 at 09:50
  • 1
    [Here's an example fiddle of this technique that demonstrates animating the attach/removal of an element without any timeouts](https://jsfiddle.net/stbjg78L/). It is in typescript can be converted to javascript by pasting into the typescript playground. No jQuery either. – Jason Kleban Jun 14 '16 at 15:39
  • 1
    This still fires twice in Chrome. Presumably because it supports webkitAnimationEnd as well as animationEnd. – Chuck Le Butt Jul 13 '16 at 16:36
  • 1
    This is great, but I wonder if there is a way to trigger this event immediately if there is no transition in progress. Imagine the case where some transition:xxx gets removed from CSS for aesthetic reasons, and then it inexplicably breaks code that was expecting this transitionend event upon changing of some style's value... – logidelic Oct 27 '16 at 20:13
25

There is an animationend Event that can be observed see documentation here, also for css transition animations you could use the transitionend event

There is no need for additional libraries these all work with vanilla JS

document.getElementById("myDIV").addEventListener("transitionend", myEndFunction);
function myEndFunction() {
 this.innerHTML = "transition event ended";
}
#myDIV {transition: top 2s; position: relative; top: 0;}
div {background: #ede;cursor: pointer;padding: 20px;}
<div id="myDIV" onclick="this.style.top = '55px';">Click me to start animation.</div>
Yehuda Schwartz
  • 3,378
  • 3
  • 29
  • 38
  • 2
    This is a borderline [link-only answer](//meta.stackexchange.com/q/8231). You should expand your answer to include as much information here, and use the link only for reference. – Blue Oct 31 '18 at 22:33
  • 9
    Voting this one up as it's the first one that doesn't rely on jQuery. No sense cutting down an entire tree for a branch. – Aaron Mason Nov 16 '18 at 03:13
  • 2
    Now browsers are more in step - for example, not needing prefixes - this vanilla Javascript solution would probably be the accepted answer nowadays (2020). Upvoted for not relying on jQuery to answer a relatively straightforward question. – A Haworth Sep 20 '20 at 06:19
  • 1
    I agree about the "Vanilla JS" which is just plain JavaScript, but the question is jQuery specific. FYI. – raphie Aug 16 '22 at 15:39
  • @raphie the way google and SO work this is where many ppl looking for the no jquery answer will end up so it makes sense that the answer should be here – Yehuda Schwartz Aug 17 '22 at 17:50
  • @YehudaSchwartz I understand, but still the question is jQuery specific, I thought SO rules stipulates that answers has to be in reference to the question. Even though the prefer answer is "Vanilla" or plain JS, the answer should be based on the question. Not on preference. I also prefer plain JS, FYI. – raphie Aug 23 '22 at 20:08
16

Another option would be to use the jQuery Transit Framework to handle your CSS3 transitions. The transitions/effects perform well on mobile devices and you don't have to add a single line of messy CSS3 transitions in your CSS file in order to do the animation effects.

Here is an example that will transition an element's opacity to 0 when you click on it and will be removed once the transition is complete:

$("#element").click( function () {
    $('#element').transition({ opacity: 0 }, function () { $(this).remove(); });
});

JS Fiddle Demo

Gaff
  • 5,507
  • 3
  • 38
  • 49
  • The callback is not working - maybe it's due to a change in transit's api I don't know, but the fiddle example is not working. It triggers the hide method before the animation runs (tried in chrome) – Jonathan Liuti Nov 21 '13 at 22:38
  • @JonathanLiuti tested using FireFox 25, IE11, Chrome 31. Works fine. – Gaff Nov 22 '13 at 16:40
  • Yea @ROFLwTIME you are totally right - it seems my chrome was just going crazy. Retested the fiddle today after a clean restart of chrome and it's working as expected. My bad ! Sorry 'bout this one. – Jonathan Liuti Nov 23 '13 at 10:06
7

For anyone that this might be handy for, here is a jQuery dependent function I had success with for applying a CSS animation via a CSS class, then getting a callback from afterwards. It may not work perfectly since I had it being used in a Backbone.js App, but maybe useful.

var cssAnimate = function(cssClass, callback) {
    var self = this;

    // Checks if correct animation has ended
    var setAnimationListener = function() {
        self.one(
            "webkitAnimationEnd oanimationend msAnimationEnd animationend",
            function(e) {
                if(
                    e.originalEvent.animationName == cssClass &&
                    e.target === e.currentTarget
                ) {
                    callback();
                } else {
                    setAnimationListener();
                }
            }
        );
    }

    self.addClass(cssClass);
    setAnimationListener();
}

I used it kinda like this

cssAnimate.call($("#something"), "fadeIn", function() {
    console.log("Animation is complete");
    // Remove animation class name?
});

Original idea from http://mikefowler.me/2013/11/18/page-transitions-in-backbone/

And this seems handy: http://api.jqueryui.com/addClass/


Update

After struggling with the above code and other options, I would suggest being very cautious with any listening for CSS animation ends. With multiple animations going on, this can get messy very fast for event listening. I would strongly suggest an animation library like GSAP for every animation, even the small ones.

David Sinclair
  • 4,187
  • 3
  • 17
  • 12
  • Thanks for share it, I have used it and edited adding `e.stopImmediatePropagation(); self.trigger(this.whichAnimationEvent()); //for purge existing event callback.apply(self);` – BomAle Jun 09 '18 at 20:56
6

The accepted answer currently fires twice for animations in Chrome. Presumably this is because it recognizes webkitAnimationEnd as well as animationEnd. The following will definitely only fires once:

/* From Modernizr */
function whichTransitionEvent(){

    var el = document.createElement('fakeelement');
    var transitions = {
        'animation':'animationend',
        'OAnimation':'oAnimationEnd',
        'MSAnimation':'MSAnimationEnd',
        'WebkitAnimation':'webkitAnimationEnd'
    };

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

$("#elementToListenTo")
    .on(whichTransitionEvent(),
        function(e){
            console.log('Transition complete!  This is the callback!');
            $(this).off(e);
        });
Chuck Le Butt
  • 47,570
  • 62
  • 203
  • 289
0

Chainable one-way events with promises

In case that you need one-way events just like JQuery's one(), I found this pattern handy:

function awaitTransitionEnd(transitionProperty, el, triggerFunction) {
    return new Promise((resolve, reject) => {
        const handler = (e) => {
            if (e.propertyName !== transitionProperty) {
                return;
            }
            el.removeEventListener('transitionend', handler);
            resolve(e);
        }
        el.addEventListener('transitionend', handler);
        triggerFunction(el);
    });
}

You can then chain CSS transitions like in this example:

awaitTransitionEnd(
    'background-color', myEl, () => myEl.classList.replace('bg-red', 'bg-green')
).then(() => awaitTransitionEnd(
    'opacity', myEl, () => myEl.classList.add('opacity-0')
)).then(() => awaitTransitionEnd(
    'opacity', myEl, () => myEl.classList.remove('opacity-0')
));

If you don't want to use arrow functions, you must pass the event + element like so:

awaitTransitionEnd('background-color', myEl, function(el) {
    el.classList.replace('bg-red', 'bg-green');
}).then(function(e) {
    return awaitTransitionEnd('opacity', e.target, function(el) {
        el.classList.add('opacity-0');
    });  
}).then(function(e) {
    return awaitTransitionEnd('opacity', e.target, function(el) {
        el.classList.remove('opacity-0');
    });
});

When awaitTransitionEnd is a class method and you don't want to use arrow functions, you must bind this to each then()-closure:

//[...]
.then(function(e) {
    return this.awaitTransitionEnd('opacity', e.target, function(el) {
        el.classList.add('opacity-0');
    });  
}.bind(this)).then(//[...]

Benni
  • 1,023
  • 11
  • 15