CSS transitions do not work when assigned trough JavaScript
Asked Answered
A

4

39

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/

Anti answered 21/11, 2011 at 10:49 Comment(0)
S
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>
Swot answered 21/11, 2011 at 14:2 Comment(5)
Thanks I really appreciate your help! I'll look into this when I get home.Anti
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 :)Anti
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?Sita
In my case I used a delay of 0 milliseconds in the setTimeout and it still works.Outcry
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?Melpomene
U
16

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.

Urana answered 13/8, 2015 at 19:52 Comment(3)
To highlight that the important line is a non-assigned, evaluated expression, you could write void element.offsetHeightAparicio
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!Urana
Thanks for the heads-up, @UranaAparicio
P
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

Polyhedron answered 20/1, 2015 at 14:44 Comment(0)
P
0

transistor09's answer is correct and should be the accepted. However, instead of relying on an obscure side effect of element.offsetHeight, I prefer to use window.requestAnimationFrame to make sure the initial state is fully applied before the target state is set:

// Set initial state
element.style.opacity = '0';
window.requestAnimationFrame(() => {
    // Delay the setting of the target state to the next animation frame.
    // The initial state has definitely been applied then.
    element.style.opacity = '1';
});

JSFiddle sample code:

const one = document.getElementById('one');
const two = document.getElementById('two');

one.style.opacity = '0';
one.style.opacity = '1';

two.style.opacity = '0';
window.requestAnimationFrame(() => {
  two.style.opacity = '1';
});
<div id="one" style="background-color: red; transition: all 3s">Transition doesn't work like that</div>
<div id="two" style="background-color: green; transition: all 3s">Works!</div>

https://jsfiddle.net/n6boapg8/

Proliferate answered 12/12, 2023 at 14:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.