CSS transitions don't work unless I use timeout - javascript

I have a couple of classes: hide is display: none, and transparent is opacity: 0. The element pr_container has -webkit-transition: opacity 1s. The following JQuery-based code makes an element appear in an animated fasion:
pr_container.removeClass("hide");
setTimeout(function() { pr_container.removeClass("transparent"); }, 0);
However, when I remove setTimeout and instead just remove the second class, there is no animation. Why?
Edit: I'm using the latest Chrome, I haven't checked other browsers yet.
Edit: I tried putting both calls in the same setTimeout callback - no animation. So it's clearly about separation.
Edit: here's the jsFiddle: http://jsfiddle.net/WfAVj/

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/

There's no reasonable "curve" to transit from one display status to another, so in current implementation of browsers, any transition that somehow involves display will end up with no transition at all.
With this code:
pr_container.removeClass("hide");
pr_container.removeClass("transparent");
You can imagine the two statements execute in a single "blocking" queue, so browsers practically renders the element from class="hide transparent" to class="", and as stated above, the hide class practically invalidates any existing transition.
By using
pr_container.removeClass("hide");
setTimeout(function() { pr_container.removeClass("transparent"); }, 0);
You told browsers to remove the "transparent" class "as soon as possible, but no in the same queue", so browser first removes "hide", and then moves on. The removal of "transparent" happens when the browser think it has resource to spare, thus the transition does not get invalidated.

only the "transperent" class produce animation .. "hide" is instant. So start the animation and if needed "hide" after 1 second:
test.addClass("transparent");
//hide after 1 sec, when the animation is done
setTimeout(function() {test.addClass("hide"); }, 1000); //1000ms = 1sec
http://jsfiddle.net/WfAVj/4/

By using suggestions in the linked question, I made a version that I'm satisfied with:
.test {
-webkit-transition: visibility 1s, opacity 1s;
}
.hide {
visibility: hidden;
}
.transparent {
opacity: 0;
}
http://jsfiddle.net/xKgjS/
Edit: now the two classes can even be combined to one!
Thanks to everyone!

Related

How to control transitions with multiple CSS class assignments from a single Javascript function?

I have one div, with buttons that trigger JS functions to add/remove CSS classes to the div. I understand this is a common way of doing simple animations ("transitions"). Try pressing the first two buttons, one after another. The red div will teleport up and become semi transparent, then fade in and slide back down. It always works.
The third button simply executes the same code as the first two buttons, but from one function. I expected it to have the same visible effect: the div immediately would jump up from the translate and be transparent, then during the course of 1 second it would slide back to its normal spot and fade in to full opacity. But it does not - the button has no visible effect.
Why? How can I make this work?
Here is the JS, see the whole thing at the codepen link.
const div = document.getElementById('red-box')
function translateUp() {
div.classList.remove('no-translate');
div.classList.add('translate-up');
}
function noTranslate() {
div.classList.remove('translate-up');
div.classList.add('no-translate');
}
//why does this function not show any transition animation?
function both() {
translateUp();
noTranslate();
}
https://codepen.io/DMcCreepy/pen/BampPyB
No jQuery please :)
The reason is that the functions are executed immediately one after the another, with no time to see the effect. transition in CSS refers to when the class is applied (could be related to a CSS state like :hover), not to switching classes via JavaScript.
To add the 1 second delay in the JS, you can use window.setTimeout:
function both() {
translateUp();
window.setTimeout(noTranslate, 1000);
}
You are relying on the div getting rerendered and all its properties being recomputed before switching the class again with the second function. I am not sure entirely what order things are done in under the hood, but I am sure you cannot rely on constructions like this working in general. It is true that switching classes is a common way to do animations and maybe it is possible to work out a solution like that, but I have provided a workaround using a CSS animation. You can read more about animations here https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations
const div = document.getElementById('red-box')
div.addEventListener('animationend', reset);
function reset() {
div.classList.remove('dropin');
}
function both() {
div.classList.add('dropin');
}
button {
display: block;
}
.box {
height: 70px;
width: 80px;
font-size: 50px;
background-color: red;
animation-duration: 1s;
animation-fill-mode: forwards;
animation-name: none;
}
.box.dropin {
animation-name: dropin;
}
#keyframes dropin {
from {
transform: translate(0, -40px);
opacity: 0.1;
}
to {
transform: translate(0, 0);
opacity: 1;
}
}
<div id="red-box" class="box">div</div>
<button onclick='both()'>Do Both</button>

CSS Transition fails on jQuery .load callback

I have a function that loads html into a table with jQuery and subsequently adds a class to one of the rows with the callback. The function is triggered by various UI driven events on the page. I also have a css transition rule so the color should fade in (transition: background-color 1000ms linear). The function looks like this:
function load_tbody(row_id) {
$('tbody').load("load_tbody.php", function() {
$(row_id).addClass('green');
});
}
Once the html is loaded, the class successfully gets added and row color is set to green. However, my css transition rule seems to be ignored.
When I add a slight delay, even 10ms, it works fine:
function load_tbody(row_id) {
$('tbody').load("load_tbody.php", function() {
setTimeout(function() {
$(row_id).addClass('green');
}, 10);
});
}
The jQuery docs for .load() state:
If a "complete" callback is provided, it is executed after
post-processing and HTML insertion has been performed.
To me this would indicate the new elements have been loaded into the dom with existing styles applied and are ready for manipulation. Why does the transition fail in the first example but succeed in the second?
Here is a fully functional example page to demonstrate the behaviour in question:
http://so-37035335.dev.zuma-design.com/
While the example above links jQuery version 2.2.3 from cdn, actual page in question uses version 1.7.1. The same behavior can be observed across both versions.
UPDATE:
After considering some of the comments and answers offered below, I've stumbled upon something altogether more confusing. User #gdyrrahitis made a suggestion which lead me to do this:
function tbody_fade(row_id) {
$('tbody').load("load_tbody.php", function() {
$('tbody').fadeIn(0, function() {
$(this).find(row_id).addClass('green');
});
});
}
Adding the class inside the fadeIn() callback works, even with a duration of 0ms. So this had me wondering... if the element is theoretically there anyway, what background color does the browser think it has before I add that class. So I log the background-color:
console.log($(row_id).css('background-color'));
And do you know what? Simply getting the background-color color made everything work:
function tbody_get_style(row_id) {
$('tbody').load("load_tbody.php", function() {
$(row_id).css('background-color');
$(row_id).addClass('green');
});
}
Just adding the line $(row_id).css('background-color'); which seemingly does nothing at all causes the transition effect to work. Here's a demo:
http://so-37035335-b.dev.zuma-design.com/
I'm just dumbfounded by this. Why does this work? Is it merely adding a small delay or does jQuery getting the css property somehow have a substantial effect on the state of the newly added element?
jQuery load is intended to drop everything that is requested into the page.
You can leverage the power of jQuery Deferred objects by using $.get instead.
Take a look at this plunk.
Code snippet from plunk
function load_tbody(row_id) {
$.when($.get("load_tbody.html", function(response) {
$('tbody').hide().html(response).fadeIn(100, function() {
$(this).find(row_id).addClass('green');
});
}));
}
I am using the $.when which will run its callback as soon the $.get is resolved, meaning will fetch the HTML response. After the response is fetched, it is appended to the tbody, which is fadedIn (fadeIn method) and after it is shown, the .green class is added to the desired row.
Note that if you go and just append the html and then the class to the row_id, you won't be able to see the transition, because it is executed immediately. A little nice visual trick with the fadeIn can do the work.
Update
On newly added elements to the DOM, CSS3 transition is not going to be triggered. This mainly happens because of the internal browser engine that controls all animations. There are numerous articles with workarounds on the issue, as well as stackoverflow answers. Additional resources can be found there, which I believe can explain the topic much better than me.
This answer is about taking a step back and changing the piece of functionality that renders dynamic elements in DOM, without going to use setTimeout or requestAnimationFrame. This is just another way to achieve what you want to achieve, in clear and consistent way, as jQuery works across browsers. The fadeIn(100, ... is what is needed to catch up with the next available frame the browser is about to render. It could be much less, the value is just to satisfy visual aesthetics.
Another workaround is to not use transitions at all and use animation instead. But from my tests this fails in IE Edge, works well on Chrome, Firefox.
Please look at the following resources:
https://www.christianheilmann.com/2015/08/30/quicky-fading-in-a-newly-created-element-using-css/
Update 2
Take a look at the specification please, as interesting stuff lies there regarding CSS3 transitions.
...This processing of a set of simultaneous style changes is called a style change event. (Implementations typically have a style change event to correspond with their desired screen refresh rate, and when up-to-date computed style or layout information is needed for a script API that depends on it.)
Since this specification does not define when a style change event occurs, and thus what changes to computed values are considered simultaneous, authors should be aware that changing any of the transition properties a small amount of time after making a change that might transition can result in behavior that varies between implementations, since the changes might be considered simultaneous in some implementations but not others.
When a style change event occurs, implementations must start transitions based on the computed values that changed in that event. If an element is not in the document during that style change even or was not in the document during the previous style change event, then transitions are not started for that element in that style change event.
When element is added, reflow is needed. The same applies to adding the class. However when you do both in single javascript round, browser takes its chance to optimize out the first one. In that case, there is only single (initial and final at the same time) style value, so no transition is going to happen.
The setTimeout trick works, because it delays the class addition to another javascript round, so there are two values present to the rendering engine, that needs to be calculated, as there is point in time, when the first one is presented to the user.
There is another exception of the batching rule. Browser need to calculate the immediate value, if you are trying to access it. One of these values is offsetWidth. When you are accessing it, the reflow is triggered. Another one is done separately during the actual display. Again, we have two different style values, so we can interpolate them in time.
This is really one of very few occasion, when this behaviour is desirable. Most of the time accessing the reflow-causing properties in between DOM modifications can cause serious slowdown.
The preferred solution may vary from person to person, but for me, the access of offsetWidth (or getComputedStyle()) is the best. There are cases, when setTimeout is fired without styles recalculation in between. This is rare case, mostly on loaded sites, but it happens. Then you won't get your animation. By accessing any calculated style, you are forcing the browser to actually calculate it
Trigger CSS transition on appended element
Explanation For the last part
The .css() method is a convenient way to get a style property from the first matched element, especially in light of the different ways browsers access most of those properties (the getComputedStyle() method in standards-based browsers versus the currentStyle and runtimeStyle properties in Internet Explorer) and the different terms browsers use for certain properties.
In a way .css() is jquery equivalent of javascript function getComputedStyle() which explains why adding the css property before adding class made everything work
Jquery .css() documentation
// Does not animate
var $a = $('<div>')
.addClass('box a')
.appendTo('#wrapper');
$a.css('opacity');
$a.addClass('in');
// Check it's not just jQuery
// does not animate
var e = document.createElement('div');
e.className = 'box e';
document.getElementById('wrapper').appendChild(e);
window.getComputedStyle(e).opacity;
e.className += ' in';
.box {
opacity: 0;
-webkit-transition: all 2s;
-moz-transition: all 2s;
transition: all 2s;
background-color: red;
height: 100px;
width: 100px;
margin: 10px;
}
.box.in {
opacity: 1;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js"></script>
<div id="wrapper"></div>
Here is the listed work arounds [SO Question]
css transitions on new elements [SO Question]
The main solution is understanding setTimeout(fn, 0) and its usages.
This is not related to CSS animations or jQuery load method.
This is a situation when you do multiple tasks into DOM.
And delay time is not important at all, the main concept is using setTimeout.
Useful answers and tutorials:
Why is setTimeout(fn, 0) sometimes useful? (stackoverflow answer)
Events and timing in-depth (real-life examples of setTimeout(fn, 0))
How JavaScript Timers Work (by jQuery Creator, John Resig)
function insertNoDelay() {
$('<tr><td>No Delay</td></tr>')
.appendTo('tbody')
.addClass('green');
}
function insertWithDelay() {
var $elem = $('<tr><td>With Delay</td></tr>')
.appendTo('tbody');
setTimeout(function () {
$elem.addClass('green');
}, 0);
}
tr { transition: background-color 1000ms linear; }
.green { background: green; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<table>
<tbody>
<tr><td>Hello World</td></tr>
</tbody>
</table>
<button onclick="insertNoDelay()">No Delay</button>
<button onclick="insertWithDelay()">With Delay</button>
This is a common problem caused by browsers. Basically, when new element is inserted it is not inserted immediately. So when you add the class, it is still not in the DOM and how it will be rendered is calculated after the class is added. When the element is added to the DOM, it already has the green background, it never had a white background so there is no transition to do. There are workarounds to overcome this as suggested here and there. I suggest you use requestAnimationFrame like this:
function load_tbody(row_id) {
$('tbody').load("load_tbody.php", function() {
requestAnimationFrame(function() {
$(row_id).addClass('green');
});
});
}
Edit:
Seems like the above solution doesn't work for all cases. I found an interesting hack here which triggers an event when an element is really parsed and added to the DOM. If you change the background color after the element is really added, the problem will not occur. Fiddle
Edit: If you want to try this solution (Won't work below IE9):
Include this CSS:
#keyframes nodeInserted {
from {
outline-color: #fff;
}
to {
outline-color: #000;
}
}
#-moz-keyframes nodeInserted {
from {
outline-color: #fff;
}
to {
outline-color: #000;
}
}
#-webkit-keyframes nodeInserted {
from {
outline-color: #fff;
}
to {
outline-color: #000;
}
}
#-ms-keyframes nodeInserted {
from {
outline-color: #fff;
}
to {
outline-color: #000;
}
}
#-o-keyframes nodeInserted {
from {
outline-color: #fff;
}
to {
outline-color: #000;
}
}
.nodeInsertedTarget {
animation-duration: 0.01s;
-o-animation-duration: 0.01s;
-ms-animation-duration: 0.01s;
-moz-animation-duration: 0.01s;
-webkit-animation-duration: 0.01s;
animation-name: nodeInserted;
-o-animation-name: nodeInserted;
-ms-animation-name: nodeInserted;
-moz-animation-name: nodeInserted;
-webkit-animation-name: nodeInserted;
}
And use this javascript:
nodeInsertedEvent= function(event){
event = event||window.event;
if (event.animationName == 'nodeInserted'){
var target = $(event.target);
target.addClass("green");
}
}
document.addEventListener('animationstart', nodeInsertedEvent, false);
document.addEventListener('MSAnimationStart', nodeInsertedEvent, false);
document.addEventListener('webkitAnimationStart', nodeInsertedEvent, false);
function load_tbody(row_id) {
$('tbody').load("load_tbody.php", function() {
$(row_id).addClass('nodeInsertedTarget');
});
}
Can be made into a generic solution or library. This is just a fast solution.

Transition not working without querying width property

I want to animate a translateX with transition on a click event by adding a class to the div in the js. The transform and transition properties are added in the css file.
var widget = document.getElementById('widget');
widget.style.display = 'block';
document.getElementById('widget2').clientWidth; //comment this line out and it wont work
widget.className = 'visible';
It only works if I query the width property of any element in the dom before adding the class.
here is a jsfiddle:
https://jsfiddle.net/5z9fLsr5/2/
Can anyone explain why this is not working?
That's because you begin your transition and modified the display property "at the same time". Altering display will ruin any transition (citation needed, admittedly), so it would be a good idea to isolate the display changing and actual transiting:
https://jsfiddle.net/5z9fLsr5/3/
document.getElementById('showWidget').addEventListener('click', function(e) {
e.preventDefault();
var widget = document.getElementById('widget');
widget.style.display = 'block';
//document.getElementById('widget2').clientWidth;
window.setTimeout(function(){
widget.className = 'visible';
},0);
});
#widget {
width: 200px;
height: 80px;
background: black;
position: absolute;
transition: transform 500ms;
transform: translateX(-200px);
display: none;
}
#widget.visible {
transform: translateX(200px);
}
#widget2 {
position: absolute;
right: 0
}
show
<div id="widget"></div>
<div id="widget2">xxx</div>
Querying clientWidth seems to "pause" the execution for some time, so it works too.
The issue here is the initial setting of display: none. To the browser's layout manager, this indicates that the layout should be done as if the element in question wasn't even in the DOM (it still is, mind you). This means that the CSS style transform: translateX(-200px); will not be applied.
Doing this:
widget.style.display = 'block';
widget.className = 'visible';
triggers both modifications essentially at the same time - the layout is only re-done after both statements have been executed. Inserting document.getElementById('widget2').clientWidth; (clientHeight works as well) triggers the layout manager to repaint, thus registering transform: translateX(-200px).
As others have mentioned before me, the solution is to either use opacity instead of display (this would be my choice), or to use setTimeout with a delay of 0 (see Why is setTimeout(fn, 0) sometimes useful?).

How to properly chain call $.show() and $.toggleClass() to trigger a CSS3 animation

I have two divs. One fills the page, the other is hidden and gets displayed with a CSS3 animation :
div{
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.div2{
top:100%;
display:none;
transition: top 0.3s ease-in-out;
}
.div2.shown{
top:0;
}
Now, I know that I cannot put display:block directly inside the .shown class because it is not supported by CSS 3 animations.
So here is what I do :
$(".div2").show().toggleClass("shown");
But the animation does not work, it just appears instantly. I thought that it was because the .show() wasn't finished yet when I trigger toggleClass. So, as a "duration" method, it must support .delay.
Now, if I delay the toggleClass :
$(".div2").show().delay(100).toggleClass("shown");
Still does not work.
Let's try a setTimeout:
$(".div2").show();
setTimeout(function(){$(".div2").toggleClass("shown")}, 1);
This one works. So it appears that show() is indeed a long operation, but it does not support the delay() method. So I wanted to try using its callback :
$(".div2").show(function(){$(".div2").toggleClass("shown")});
And this works, without setTimeout ! But the callback gets called twice (once at the beginning, once 400ms after).
What should I use to do that properly ?
Took me a second to see this. The easiest solution is just to set a timer of 0 on your $.show() command, like:
$('.div2').show(0).toggleClass('shown');
That will toggle the show instantly and allow for the animation to happen (see http://jsfiddle.net/8y4Lq2n9/).
If you're worried about setting a specific number, there are options you can pass into the show command, with the specific one being :
$('.div2').show({queue:false}).toggleClass('shown');
This will take the element out of the animation queue and allow other actions to happen instead of preventing the transitions. See: http://jsfiddle.net/8y4Lq2n9/1/

Css animation - setTimeout doesn't apply changes which breaks animation

Here is the description of my goal:
I've got box with display: none
At some moment I need to display it with opacity animation.
Here my solution:
1. transition: opacity 0.5s ease-in-out;
2. On first step set display: block; opacity: 0
3. On second step set display: block; opacity: 1, do it in setTimeout() to apply first step.
The problem is that first step applied only in some cases - sometimes it works / sometimes doesn't and browser just skips first step. I thought changing setTimeout to requestAnimationFrame should fix the problem but it doesn't - check my example
Why setTimeout / requestAnimationFrame does not force browser to apply first step? How to force browser to apply first step before applying second one?
Solution: http://jsfiddle.net/sxny7zs2/
.box{display:none} should be .box{display: block;}
When you set display:none you remove the object from the DOM almost entirely. By resetting to display:block you bring the object back fully and it begins to interact with other objects. The display feature is not meant for animations but for removing objects from interfering with others.
I suspect this is the villain:
$box.removeClass('is-animate-enter').addClass('is-animate-active');
By removing is-animate-enter class you trigger display:none; before you are able to add your next class. This means the object is unloaded from the view. Meanwhile when you do is-animate-active you instantly set display:block and opacity:1. As far as the browser is concerned you are creating a new element, not modifying an old one here. As previously stated, when toggling the display you are actually loading and unloading an object so no animation is possible.
Maybe .switchClass() could fix this but I'm not sure, to reiterate the display command is for loading and unloading and not for animations.

Categories

Resources