38

I'm having some major headache trying to apply CSS3 transitions to a slideshow trough JavaScript.

Basically the JavaScript gets all of the slides in the slideshow and applies CSS classes to the correct elements to give a nice animated effect, if there is no CSS3 transitions support it will just apply the styles without a transition.

Now, my 'little' problem. All works as expected, all slides get the correct styles, the code runs without bugs (so far). But the specified transitions do not work, even though the correct styles where applied. Also, styles and transitions work when I apply them myself trough the inspector.

Since I couldn't find a logical explanation myself I thought someone here could answer it, pretty please?

I've put together a little example of what the code is right now: http://g2f.nl/38rvma Or use JSfiddle (no images): http://jsfiddle.net/5RgGV/1/

ThinkingStiff
  • 64,767
  • 30
  • 146
  • 239
Jon Koops
  • 8,801
  • 6
  • 29
  • 51

3 Answers3

65

To make transition work, three things have to happen.

  1. the element has to have the property explicitly defined, in this case: opacity: 0;
  2. the element must have the transition defined: transition: opacity 2s;
  3. the new property must be set: opacity: 1

If you are assigning 1 and 2 dynamically, like you are in your example, there needs to be a delay before 3 so the browser can process the request. The reason it works when you are debugging it is that you are creating this delay by stepping through it, giving the browser time to process. Give a delay to assigning .target-fadein:

window.setTimeout(function() {
  slides[targetIndex].className += " target-fadein";
}, 100); 

Or put .target-fadein-begin into your HTML directly so it's parsed on load and will be ready for the transition.

Adding transition to an element is not what triggers the animation, changing the property does.

// Works
document.getElementById('fade1').className += ' fade-in'

// Doesn't work
document.getElementById('fade2').className = 'fadeable'
document.getElementById('fade2').className += ' fade-in'

// Works
document.getElementById('fade3').className = 'fadeable'

window.setTimeout(function() {
  document.getElementById('fade3').className += ' fade-in'
}, 50)
.fadeable {
  opacity: 0;
}

.fade-in {
  opacity: 1;
  transition: opacity 2s;
}
<div id="fade1" class="fadeable">fade 1 - works</div>
<div id="fade2">fade 2 - doesn't work</div>
<div id="fade3">fade 3 - works</div>
Jon Koops
  • 8,801
  • 6
  • 29
  • 51
ThinkingStiff
  • 64,767
  • 30
  • 146
  • 239
  • 1
    Thanks I really appreciate your help! I'll look into this when I get home. – Jon Koops Nov 21 '11 at 15:29
  • 1
    Yup, seems to be the only thing to do. Too bad that there isn't an event for when styles to be applied. I guess I just have to wait for 100ms :) – Jon Koops Nov 27 '11 at 14:51
  • I just ran into this problem and it took me hours to figure out that some time needs to pass after applying the transition property. Is there anything known about WHY this is the case, since usually styles are applied instantly? Is this a bug or intended behaviour? – BlackWolf Jan 20 '14 at 11:57
  • 2
    In my case I used a delay of `0` milliseconds in the `setTimeout` and it still works. – nonzaprej Dec 14 '17 at 16:20
  • 1
    Is there a way to do it with `promise` or would we not be able to do it because the className assignment is not an async object? – Isa Sep 29 '19 at 00:38
15

Trick the layout engine!

function finalizeAndCleanUp (event) {
    if (event.propertyName == 'opacity') {
        this.style.opacity = '0'
        this.removeEventListener('transitionend', finalizeAndCleanUp)
    }
}
element.style.transition = 'opacity 1s'
element.style.opacity = '0'
element.addEventListener('transitionend', finalizeAndCleanUp)
// next line's important but there's no need to store the value
element.offsetHeight
element.style.opacity = '1'

As already mentioned, transitions work by interpolating from state A to state B. If your script makes changes in the same function, layout engine cannot separate where state A ends and B begins. Unless you give it a hint.

Since there is no official way to make the hint, you must rely on side effects of some functions. In this case .offsetHeight getter which implicitly makes the layout engine to stop, evaluate and calculate all properties that are set, and return a value. Typically, this should be avoided for performance implications, but in our case this is exactly what's needed: state consolidation.

Cleanup code added for completeness.

transistor09
  • 4,828
  • 2
  • 25
  • 33
  • 2
    To highlight that the important line is a non-assigned, evaluated expression, you could write `void element.offsetHeight` – trincot Oct 28 '16 at 23:20
  • 1
    Word of caution about `transitionend`: this event is not guaranteed to fire. The browser may optimize it away if, for example, the tab is not visible. Do not rely on it or have a fallback! – transistor09 Oct 29 '16 at 14:07
  • Thanks for the heads-up, @transistor09 – trincot Oct 29 '16 at 14:11
4

Some people have asked about why there is a delay. The standard wants to allow multiple transitions, known as a style change event, to happen at once (such as an element fading in at the same time it rotates into view). Unfortunately it does not define an explicit way to group which transitions you want to occur at the same time. Instead it lets the browsers arbitrarily choose which transitions occur at the same time by how far apart they are called. Most browsers seem to use their refresh rate to define this time.

Here is the standard if you want more details: http://dev.w3.org/csswg/css-transitions/#starting