Clean Way to Programmatically Use CSS Transitions from Js

Clean way to programmatically use CSS transitions from JS?

There isn't a clean way at this moment (without using CSS Animations -- see the nearby answer by James Dinsdale for an example using CSS Animations). There is a spec bug 14617, which unfortunately wasn't acted upon since it was filed in 2011.

setTimeout does not work reliably in Firefox (this is by design).

I'm not sure about requestAnimationFrame -- an edit to the original question says it doesn't work reliably either, but I did not investigate. (Update: it looks like requestAnimationFrame is considered at least by one Firefox core developer to be the place where you can make more changes, not necessarily see the effect of the previous changes.)

Forcing reflow (e.g. by accessing offsetHeight) is a possible solution, but for transitions to work it should be enough to force restyle (i.e. getComputedStyle): https://timtaubert.de/blog/2012/09/css-transitions-for-dynamically-created-dom-elements/

window.getComputedStyle(elem).opacity;

Note that just running getComputedStyle(elem) is not enough, since it's computed lazily. I believe it doesn't matter which property you ask from the getComputedStyle, the restyle will still happen. Note that asking for geometry-related properties might cause a more expensive reflow.

More information on reflow/restyle/repaint: http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/

What is the cleanest way to disable CSS transition effects temporarily?

Short Answer

Use this CSS:

.notransition {
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
transition: none !important;
}

Plus either this JS (without jQuery)...

someElement.classList.add('notransition'); // Disable transitions
doWhateverCssChangesYouWant(someElement);
someElement.offsetHeight; // Trigger a reflow, flushing the CSS changes
someElement.classList.remove('notransition'); // Re-enable transitions

Or this JS with jQuery...

$someElement.addClass('notransition'); // Disable transitions
doWhateverCssChangesYouWant($someElement);
$someElement[0].offsetHeight; // Trigger a reflow, flushing the CSS changes
$someElement.removeClass('notransition'); // Re-enable transitions

... or equivalent code using whatever other library or framework you're working with.

Explanation

This is actually a fairly subtle problem.

First up, you probably want to create a 'notransition' class that you can apply to elements to set their *-transition CSS attributes to none. For instance:

.notransition {
-webkit-transition: none !important;
-moz-transition: none !important;
-o-transition: none !important;
transition: none !important;
}

(Minor aside - note the lack of an -ms-transition in there. You don't need it. The first version of Internet Explorer to support transitions at all was IE 10, which supported them unprefixed.)

But that's just style, and is the easy bit. When you come to try and use this class, you'll run into a trap. The trap is that code like this won't work the way you might naively expect:

// Don't do things this way! It doesn't work!
someElement.classList.add('notransition')
someElement.style.height = '50px' // just an example; could be any CSS change
someElement.classList.remove('notransition')

Naively, you might think that the change in height won't be animated, because it happens while the 'notransition' class is applied. In reality, though, it will be animated, at least in all modern browsers I've tried. The problem is that the browser is caching the styling changes that it needs to make until the JavaScript has finished executing, and then making all the changes in a single reflow. As a result, it does a reflow where there is no net change to whether or not transitions are enabled, but there is a net change to the height. Consequently, it animates the height change.

You might think a reasonable and clean way to get around this would be to wrap the removal of the 'notransition' class in a 1ms timeout, like this:

// Don't do things this way! It STILL doesn't work!
someElement.classList.add('notransition')
someElement.style.height = '50px' // just an example; could be any CSS change
setTimeout(function () {someElement.classList.remove('notransition')}, 1);

but this doesn't reliably work either. I wasn't able to make the above code break in WebKit browsers, but on Firefox (on both slow and fast machines) you'll sometimes (seemingly at random) get the same behaviour as using the naive approach. I guess the reason for this is that it's possible for the JavaScript execution to be slow enough that the timeout function is waiting to execute by the time the browser is idle and would otherwise be thinking about doing an opportunistic reflow, and if that scenario happens, Firefox executes the queued function before the reflow.

The only solution I've found to the problem is to force a reflow of the element, flushing the CSS changes made to it, before removing the 'notransition' class. There are various ways to do this - see here for some. The closest thing there is to a 'standard' way of doing this is to read the offsetHeight property of the element.

One solution that actually works, then, is

someElement.classList.add('notransition'); // Disable transitions
doWhateverCssChangesYouWant(someElement);
someElement.offsetHeight; // Trigger a reflow, flushing the CSS changes
someElement.classList.remove('notransition'); // Re-enable transitions

Here's a JS fiddle that illustrates the three possible approaches I've described here (both the one successful approach and the two unsuccessful ones):
http://jsfiddle.net/2uVAA/131/

css transitions on new elements

requestAnimationFrame() (https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame) appears to work across Firefox, Chrome and Safari. A more reliable, logical solution that setTimeout(). For older browsers (IE8), it will require a Polyfill (naturally, the transition won't occur, but the CSS will still change).

CSS transitions do not work when assigned trough JavaScript

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>

CSS transitions don't work unless I use timeout

You can't make a transition if you are changing display property at the same time. So in order to make it work you have to hide your element some other way. For example:

.hide {
height: 0;
width: 0;
/* overflow: hidden; padding: 0; border: none; */
}

http://jsfiddle.net/dfsq/WfAVj/1/

Can I start this simple CSS transition without using javascript

I changed your class name to match the new behavior (animation, not a transition). We're animating from 100px to the full width of the viewport.

html, body { margin: 0; } /* Prevent scrollbar */
div.simple_animation { width: 100px; height: 100px; background: red; animation: 5s loading forwards;}
@keyframes loading { to { width: 100vw; }}
<div class="simple_animation"></div>

Is Chrome buggy when it comes to css transitions?

From All you need to know about CSS Transitions:

[...] you’ll find that if you apply both sets of properties, one immediately after the other, then the browser tries to optimize the
property changes, ignoring your initial properties and preventing a
transition. Behind the scenes, browsers batch up property changes
before painting which, while usually speeding up rendering, can
sometimes have adverse affects.

The solution is to force a redraw between applying the two sets of properties. A simple method of doing this is just by accessing a DOM
element’s offsetHeight property [...]

When your code invokes .css("height"), jQuery internally access the element's offsetHeight property which forces a redraw. Without this, Blink and Webkit will batch up the styling changes made by .css("height", r) and .addClass("test").

If you don't like the call to .css("height"), you can replace it with anything else that forces a redraw, e.g. accessing the element's offsetHeight property:

this.offsetHeight;

Fiddle

I've asked a similar question looking for a clean solution but apparently there's none yet.

Firefox had a similar behavior until not long ago (Firefox 23 had the same "bug") but it was fixed, so we can hope that Blink and Webkit may fix it in the future.

Opacity fade works only when defered

First case

document.getElementById("toto").style.opacity = 0;
document.getElementById("toto").style.transition = "all 5s ease";
document.getElementById("toto").style.opacity = 1;

This happens because each line in above javascript is invoked immediately and it is similar to setting these css rules:

#toto {
opacity: 0;
transition: all 5s ease;
opacity: 1;
}

as you can see the second occurence of opacity will override the value 0, so in result it will be 1.

#toto {
transition: all 5s ease;
opacity: 1;
}

There is no animation in this case just static opacity that isn't going to change.

Second case

document.getElementById("toto").style.opacity = 0;
window.setTimeout(function(){
document.getElementById("toto").style.transition = "all 5s ease";
document.getElementById("toto").style.opacity = 1;
}, 1);

In this case you initially set the opacity to 0. And after 1ms you change the css rules, so there is a change in opacity from 0 to 1. That's why the animation was fired.

Regards.



Related Topics



Leave a reply



Submit