Categories:

Using transitionend event to detect when a transition has finished

A likely part of many complex CSS animations involves doing something after one transition has ended. Estimating when this has occurred- as we've done above using a setTimeout() function on the previous page- is both unreliable and unmanageable, especially if we wish to keep track of multiple transitions. This is where the transitionend event comes in- a JavaScript event that fires whenever, well, you've guessed it, a transition has ended. This event is supported in IE10+ and modern versions of other browsers, including on the mobile front.

transitionend can be attached to an element like any other in jQuery, with a couple of caveats:

$(element).on('transitionend webkitTransitionEnd', function(e){
 // access original Event object versus jQuery's normalized version
 var evt = e.originalEvent
})

Firstly, notice the presence of "webkitTransitionEnd" in addition to the standard "transitionend" event when hooking up with the event handler. This is necessary in order for Safari (as of Safari v5.1.7 and jQuery 2.0) to properly recognize the event, despite the fact that jQuery 1.8+ automatically normalizes the majority of other CSS3 property and event names. In this case, it seems a little additional nudge in the right direction is necessary. Secondly, inside the event function, instead of the typical route of accessing jQuery's normalized version of the event object, or e, you'll want to instead reference the untouched, original Event object, which jQuery thoughtfully makes available via e.originalEvent. This is because, as of jQuery 2.0, most of the properties associated with the original Event object during the "transitionend" event have yet to be ported over to the normalized version's, so we must rely on the former instead.

The transitionend event bubbles, which is good news, as it means we can define our event handler function higher up in the chain of nodes from the elements firing the event, and handle them all easily without having to define multiple transitionend event handlers.

When a transitionend event fires, the Event object is populated with the following properties:

Property Description
target The event target; in other words, the element that actually fired this transitionend event.
type The type of event, in this case, the string "transitionend".
canBubble Boolean indicating whether this event bubbles.
cancelable Boolean indicating whether it's possible to cancel this event.
propertyName The name of the CSS property being transitioned. In a CSS transition where there are multiple CSS properties being transitioned, transitionend fires once for each of these properties, one after another.
elapsedTime The amount of time this transition has been running when the event (in this case, transitionend) fires, in seconds.
pseudoElement The name of the CSS pseudo-element (beginning with two colons, such as ":before", ":after", ":first-letter" etc) on which the transition occurred, or the empty string if the transition occurred on the element itself.

One of the most important points to remember here is that transitionend fires once for each transitioned property within the associated element, and not just once for the entire element. In the case of the below CSS selector for example:

h3:hover{
font-size: 120%;
color: red;
background: black;
-moz-transition: all 0.3s ease-in;
-webkit-transition: all 0.3s ease-in;
-ms-transition: all 0.3s ease-in;
transition: all 0.3s ease-in;
}

When you hover over a H3 element to trigger the transition effect, you'll find that the transitionend event for the element will have fired at least 3 times, once for "font-size", once for "color", and once for "background". Why at a minimum? Because depending on the browser, other related CSS properties may also be transitioned indirectly as a result, such as "margin-top" and "margin-bottom" due to the font size being changed. The transitionend event will also fire once for each of these properties as well, which you can see exactly what by examining the propertyName property of the Event object. The bottom line is, transitionend fires not once on an element each time there is a transition occurring on it, but once for every property that has transitioned on the element.

Refactoring our dropping text example to use transitionend

The best way to see how to use transitionend is to see it in action, and we have the perfect candidate to operate on, our previous dropping text example. Recall that it used a crude technique involving setTimeout() to estimate when the transition has finished to reset it:

<script>

jQuery(function(){
 var $header2 = $('#header2')
 $header2.lettering() // wrap <span class="charx"/ > around each character within header
 var $spans = $header2.find('span')
 var delay = 0
 $header2.on('click', function(){
 $spans.each(function(){
  $(this).css({transitionDelay: delay+'s'}) // apply sequential trans delay to each character
  delay += 0.1
 })
 $header2.addClass('dropped') // Add "dropped" class to header to apply transition
 setTimeout(function(){ // reset header code
  $spans.each(function(){
   $(this).css({transitionDelay: '0ms'}) // set transition delay to 0 so when 'dropped' class is removed, letter appears instantly
  })
  $header2.removeClass('dropped') // remove class at the "end" to reset header.
  delay = 0
  }, 1800) // 1800 is just rough estimate of time transition will finish, not the best way

 })
})

</script>

<body>

<h3 id="header2" class="header">Click &nbsp;on &nbsp;me!</h3>
</body>

Lets bid farewell to the code in red and utilize transitionend instead to accomplish the following:

  • Keep track of precisely when the transition on each letter has ended, and reset the transition-delay value on that letter to 0.
  • Keep a tally of how many transitions have ended, and when it matches the total number of letters, reset the entire animation by removing the "dropped" class from the parent container.

Demo:

Click  on  me!

Here's the improved version:

<style>

#header2{
font-family: 'Orbitron', sans-serif; /* font style. Default uses Google fonts */
text-shadow: 2px 2px #B4EAF3, 3px 3px #B4EAF3, 4px 4px #B4EAF3, 5px 5px #B4EAF3, 6px 6px #B4EAF3;
font-size: 64px; /* font size of text */
color: #207688;
letter-spacing: 15px;
font-weight: 800;
text-transform: uppercase;
position: relative;
}

#header2 span{
display: inline-block;
}

.dropped span{
-moz-transform: translateY(300px) rotateZ(120deg); /* rotate, translate, and disappear */
-webkit-transform: translateY(300px) rotateZ(120deg);
transform: translateY(300px) rotateZ(120deg);
opacity: 0;
-moz-transition: all 0.3s ease-in;
-webkit-transition: all 0.3s ease-in;
-ms-transition: all 0.3s ease-in;
transition: all 0.3s ease-in;
}

</style>

<script>

jQuery(function(){
 var $header2 = $('#header2')
 $header2.lettering() // wrap <span class="charx"/ > around each character within header
 var $spans = $header2.find('span')
 var delay = 0
 var transitionsfired = 0
 var transitionisrunning = false
 $header2.on('transitionend webkitTransitionEnd', function(e){
  var $target = $(e.target) // target letter transitionend fired on
  if (/transform/i.test(e.originalEvent.propertyName)){ // check event fired on "transform" prop
   transitionsfired += 1
   $target.css({transitionDelay:'0ms'}) // set transition delay to 0 so when 'dropped' class is removed, letter appears instantly
   if (transitionsfired == $spans.length){ // all transitions on characters have completed?
    transitionsfired = 0 // reset number of transitions fired
    delay = 0
    $header2.removeClass('dropped')
    transitionisrunning = false // indicate transition has stopped
   }
  }
 })

 $header2.on('click', function(){
  if (transitionisrunning === false){
   $spans.each(function(){
    $(this).css({transitionDelay: delay+'s'}) // apply sequential trans delay to each character
    delay += 0.1
   })
   $header2.addClass('dropped') // Add "dropped" class to header to apply transition
   transitionisrunning = true // indicate transition is running
  }
 })
})

</script>

<h3 id="header2" class="header">Click &nbsp;on &nbsp;me!</h3>

The demo may look identical to its predecessor on the previous page in terms of behaviour, but it's vastly more reliable and versatile. No longer are we guessing when each transition has ended, which when it comes to coding is never a good thing (animation prematurely cutting off, longer than necessary delay before reset, the end of the World etc). Lets examine the innards of the above script to see how everything comes together:

  • Near the end of the code, we add the class "dropped" to the header, kicking off the CSS3 transition.
  • We attach the transitionend event to the H2 container of our text, so it monitors transitions that occur on all the individual characters inside it (since the event bubbles).
  • We reference the target character the transitionend actually fired on (each character is wrapped in its own span tag) with e.target before making it a jQuery object; in this case it's not necessary to use e.originalEvent.target, as the former isn't a transitionend specific property that jQuery has yet to properly normalize.
  • Each time transitionend fires, we pick one of the properties being transitioned on the target- in this case transform- to tell us the transition has completed in general over the target. We could have gone with any other property inside the "dropped" class- properties that are part of the transition. Recall that transitionend fires multiple times on an element based on the number of properties there are that are being transitioned. By picking one of these properties to see when its transition has ended, we essentially know when the entire transition on the corresponding target character has ended. When that happens, we increment the transitionsfired variable by one and set the character's transition-delay property to 0 (so there's no animation when the "dropped" class is removed at the end).
  • We compare the variable transitionsfired against the number of characters in the header- if they match, that means the transitions on all of the characters have completed. When that happens, we reset some variables that help keep tabs on various aspects of the transitions, and just as importantly, remove the "dropped" class from the header, resetting the entire animation in the process.

The role of JavaScript when it comes to CSS3 transitions may be a supporting one, but depending on the scene, a vital one at the same time. With the ability to dynamically set CSS3 properties and respond to a transition ending, the stage is set for more intricate CSS3 effects!

End of Tutorial