5

I have 2 divs, one of them is hidden via display:none;. Both have the same css transition on the property right.

If I change the property right through JQuery and show the hidden div, either by using $.css('display','none') or $.show() or $.toggle() etc., the hidden div draw instantly at the ending position

$('button').on('click',function(){
  $('.b').show();
  $('.b').css('right','80%');
  $('.a').css('right','80%');
})
body {
  width:800px;
  height:800px;
}

div {
  width:50px;
  height:50px;
  background-color:#333;
  position:absolute;
  display:none;
  right:5%;
  top:0;
  transition:right .5s cubic-bezier(0.645, 0.045, 0.355, 1);
  color: white;
}

.a {
  display:block;
  top:60px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class='a'>A
</div>
<div class='b'>B
</div>
<button>Launch</button>

If I use $.animate() it will work. But my question is ; Is that a bug or a normal behavior?

Edit Not a duplicate of Transitions on the display: property cause the problem here is not about animating the display property nor the visibility

yunzen
  • 32,854
  • 11
  • 73
  • 106
Richard
  • 994
  • 7
  • 26
  • normal behavior, you need a slight delay between the display and changing the right – Temani Afif Jan 24 '19 at 11:00
  • I was about to say "ok", but what do you call a normal behavior here? Something that doesn't work can't be a normal behavior right? It's just a bug, a well known bug, I can understand that and I'll deal with it, but still a bug, no? – Richard Jan 24 '19 at 11:50
  • This is NOT a duplicate. How can I deny this? – Richard Jan 24 '19 at 11:55
  • it's not a bug but it's how it should work ... and yes it's not a duplicate. I don't have the needed words to explain it but someone else will come and better explain – Temani Afif Jan 24 '19 at 11:59
  • check this: https://jsfiddle.net/r7016skg/ adding a small delay will output what you expect – Temani Afif Jan 24 '19 at 12:01

5 Answers5

14

To understand plainly the situation, you need to understand the relation between the CSSOM and the DOM.

In a previous Q/A, I developed a bit on how the redraw process works.
Basically, there are three steps, DOM manipulation, reflow, and paint.

  • The first (DOM manipulation) is just modifying a js object, and is all synchronous.
  • The second (reflow, a.k.a layout) is the one we are interested in, and a bit more complex, since only some DOM methods and the paint operation need it. It consists in updating all the CSS rules and recalculating all the computed styles of every elements on the page.
    Being a quite complex operation, browsers will try to do it as rarely as possible.
  • The third (paint) is only done 60 times per seconds at max (only when needed).

CSS transitions work by transitioning from a state to an other one. And to do so, they look at the last computed value of your element to create the initial state.
Since browsers do recalculate the computed styles only when required, at the time your transition begins, none of the DOM manipulations you applied are effective yet.

So in your first scenario, when the transition's initial state is calculated we have

.b { computedStyle: {display: none} }

... and that's it.

Because, yes, that's how powerful display: none is for the CSSOM; if an element has display: none, then it doesn't need to be painted, it doesn't exist.

So I'm not even sure the transition algorithm will kick in, but even if it did, the initial state would have been invalid for any transitionable value, since all computed values are just null.

Your .a element being visible since the beginning doesn't have this issue and can be transitioned.

And if you are able to make it work with a delay (induced by $.animate), it's because between the DOM manip' that did change the display property and the execution of this delayed DOM manip' that does trigger the transition, the browser did trigger a reflow (e.g because the screen v-sync kicked in between and that the paint operation fired).


Now, it is not part of the question, but since we do understand better what happens, we can also control it better.

Indeed, some DOM methods do need to have up-to-date computed values. For instance Element.getBoundingClientRect, or element.offsetHeight or getComputedStyle(element).height etc. All these need the entire page to have updated computed values so that the boxing are made correctly (for instance an element could have a margin pushing it more or less, etc.).

This means that we don't have to be in the unknown of when the browser will trigger this reflow, we can force it to do it when we want.

But remember, all the elements on the page needs to be updated, this is not a small operation, and if browsers are lenient to do it, there is a good reason.

So better use it sporadically, at most once per frame.

Luckily, the Web APIs have given us the ability to hook some js code just before this paint operation occurs: requestAnimationFrame.

So the best is to force our reflow only once in this pre-paint callback, and to call everything that needs the updated values from this callback.

$('button').on('click',function(){
  $('.b').show(); // apply display:block synchronously
  
  requestAnimationFrame(() => { // wait just before the next paint
    document.body.offsetHeight; // force a reflow
    // trigger the transitions
    $('.b').css('right','80%');
    $('.a').css('right','80%');
  });
})
body {
  width:800px;
  height:800px;
}

div {
  width:50px;
  height:50px;
  background-color:#333;
  position:absolute;
  display:none;
  right:5%;
  top:0;
  transition:right .5s cubic-bezier(0.645, 0.045, 0.355, 1);
  color: white;
}

.a {
  display:block;
  top:60px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class='a'>A
</div>
<div class='b'>B
</div>
<button>Launch</button>

But to be honest, it's not always easy to set up, so if you are sure it is something that will get fired sporadically, you may be lazy and do it all synchronously:

$('button').on('click',function(){
  $('.b').show(); // apply display:block
  document.body.offsetHeight; // force a reflow
  // trigger the transitions
  $('.b').css('right','80%');
  $('.a').css('right','80%');
})
body {
  width:800px;
  height:800px;
}

div {
  width:50px;
  height:50px;
  background-color:#333;
  position:absolute;
  display:none;
  right:5%;
  top:0;
  transition:right .5s cubic-bezier(0.645, 0.045, 0.355, 1);
  color: white;
}

.a {
  display:block;
  top:60px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class='a'>A
</div>
<div class='b'>B
</div>
<button>Launch</button>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Ok, I mark this as best. This is very instructive, thank you! If I understand correctly both what you explained and the jquery doc for `$.show()`, the @yunzen is correct because calling `$.show()` as an animation will trigger a reflow at the end right? (`$.show(0)`) – Richard Jan 24 '19 at 15:20
  • @vincent-d I couldn't get my head clearly on what happens in jQuery sources when duration is `0`, but yes, there must be something there that does trigger a reflow. If you have more time than myself to make the walkthrough, the entrance [is here](https://github.com/jquery/jquery/blob/e743cbd28553267f955f71ea7248377915613fd9/src/effects.js#L633). – Kaiido Jan 25 '19 at 01:44
  • @Kaiido Thank you again that was very instructive and interesting. I'm reading jquery to figure this out, if I find, I'll tell you. Any way, what is sure is there's a reflow when you use `$.show()` passing params throught it, because of this comment `Set the display of the elements in a second loop to avoid constant reflow` So what I'm looking at are the globals and I found `ownerDocument()` and `appendChild()`but I can't find them in the list you provided here https://gist.github.com/paulirish/5d52fb081b3570c81e3a – Richard Jan 25 '19 at 09:10
  • Oh, and the appenChild() come imediatly with a `removeChild()`. That happens each time you call `show()` but not if you put no param because it will just juggling with the display property (and apparently that avoid the reflow) – Richard Jan 25 '19 at 09:13
  • @Kaiido Maybe my javascript knowledge is a little poor, so tell me if that may be the reason of the reflow :D – Richard Jan 25 '19 at 09:15
1

jQuery show() without a parameter basically does this

$('.b').css('display', 'block');

So what you are doing is this

$('button').on('click',function(){
  $('.b').css('display', 'block');
  $('.b').css('right','80%');
  $('.a').css('right','80%');
})

which is basically the same as

$('button').on('click',function(){
  $('.b').css({
    'display': 'block',
    'right': '80%'
  });
  $('.a').css('right','80%');
})

But you can't transition anything, if the display is changed at the same time.

Adding a duration value to the show() call, does something more complex than just changing the display. It will add the element to an animation queue like this

$('.b').css({
  'display': 'block', 
  'overflow': 'hidden',
  'height': '0',
  'width': '0',
  'margin': '0',
  'width': '0',
  'opacity': '0'
});
$('.b').animate({
  height: '50px',
  padding: '0px',
  margin: '0px', 
  width: '50px', 
  opacity: '1'
}, 0)

So what you need to do is putting the duration value of 0 (zero) into the show() call as a parameter

$('button').on('click',function(){
  $('.b').show(0);
  $('.b').css('right','80%');
  $('.a').css('right','80%');
})
body {
  width:800px;
  height:800px;
}

div {
  width:50px;
  height:50px;
  background-color:#333;
  position:absolute;
  display:none;
  right:5%;
  top:0;
  transition:right .5s .1s cubic-bezier(0.645, 0.045, 0.355, 1);
  color: white;
}

.a {
  display:block;
  top:60px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class='a'>A
</div>
<div class='b'>B
</div>
<button>Launch</button>
yunzen
  • 32,854
  • 11
  • 73
  • 106
  • 1
    from the Doc: `The matched elements will be revealed immediately, with no animation. This is roughly equivalent to calling .css( "display", "block" ),` http://api.jquery.com/show/ – Temani Afif Jan 24 '19 at 12:16
  • 1
    by adding a number you make it animated and fix the issue (any number will work, even higher than 0) – Temani Afif Jan 24 '19 at 12:17
  • @TemaniAfif That's what the doc says. It also says the default duration is `400` . I'm confused – yunzen Jan 24 '19 at 12:27
  • 1
    there is different call of the method ... with and without argument. the on without is not animated and the one with argument is animated ... and the animated one is fixing the issue – Temani Afif Jan 24 '19 at 12:29
  • This is weird yes, but the result is perfect. Before I came back here (I thought my post was lost forever lol) I came up with the show() callback. And yes, it does apply the 400ms as @TemaniAfif explained. So I validate this solution since it's the best imho – Richard Jan 24 '19 at 13:15
0

Personally I would check to see if the element is visible after making it show. Then continue on through the jQuery.

Like this: https://jsfiddle.net/789su6xb/

$('button').on('click',function(){

    var b = $('.b');
    b.show();

    if(b.is(":visible")) {
        $('.b').css('right','80%');
        $('.a').css('right','80%');
    }

});
Jason Is My Name
  • 929
  • 2
  • 14
  • 38
  • Please explain your down vote. This is an acceptable answer. – Jason Is My Name Jan 24 '19 at 12:41
  • I did not downvote, but I understand why. I said I don't want to use $.animate and let the css transition do his job, so if I don't want that, I don't either want a solution where I have to check the dom status – Richard Jan 24 '19 at 13:22
  • Your question is very unclear. Everyone is getting downvotes for providing improved code to what you currently use. You ask if it is normal behavior, yes. Your code just needs to be written like one of our answers. You need to state that the show is instant. Provide a delay. Or check if your element is visible. – Jason Is My Name Jan 24 '19 at 13:40
  • How my question is unclear? Also, check the validate answer, this is a good answer because it's a solution to the problem, not a sneaky way to get a similar "good enough" result. – Richard Jan 24 '19 at 13:49
  • I don't know why you get a downvote, you don't derserve it, still your answer is not good enough to be the good one, and I told you why. The others get downvotes because they answers are totaly 100% off-topic – Richard Jan 24 '19 at 13:51
  • Very kind. I still don't understand what you're asking, haha. Have you received an answer to your question now? – Jason Is My Name Jan 24 '19 at 14:15
  • Yes, you don't see it? Basicaly, when you change the display of an element, at the very same time you do it you can't do any transitions on that element. `$.show()` method have multiple call status, one of them is an animation itself, that's what we need to fix the "native" behavior I just explained (display vs other anim) so we simply set a duration to $.show() to get that call, by doing `$.show(0)` – Richard Jan 24 '19 at 14:33
  • Agreed. Bless you for sticking at this awkward question. God speed! – Jason Is My Name Jan 24 '19 at 17:11
0

The problem with starting a transition on show is the same issue as trying to start a transition on load. It's really annoying that CSS doesn't support starting an animation on the same frame that an element shows. See the answer here: css3 transition animation on load?

The solution is to use keyframes instead of transition. But then you can't do a variable right, it's hard coded at 80% in the CSS file.

$('button').on('click',function(){
  $('.b').show();
  $('.b').addClass('goLeft');
  $('.a').addClass('goLeft');
})
body {
  width:800px;
  height:800px;
}

div {
  width:50px;
  height:50px;
  background-color:#333;
  position:absolute;
  display:none;
  right:5%;
  top:0;
  color: white;
}

.goLeft {
  animation-name: goLeft;
  animation-duration: .5s;
  animation-iteration-count: 1;
  animation-play-state: running;
  animation-fill-mode: forwards;
  animation-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1);
}

.a {
  display:block;
  top:60px;
}

@keyframes goLeft {
  from {
    right: 5%;
  }
  to {
    right: 80%;
  }
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class='a'>A
</div>
<div class='b'>B
</div>
<button>Launch</button>
Curtis
  • 2,486
  • 5
  • 40
  • 44
-1

Check this fiddle Just need to add a timeout

$('button').on('click',function(){
  $('.b').show();
  setTimeout(function(){ $('.b').css('right','80%');
  $('.a').css('right','80%'); }, 0);
  
})
body {
  width:800px;
  height:800px;
}

div {
  width:50px;
  height:50px;
  background-color:#333;
  position:absolute;
  display:none;
  right:5%;
  top:0;
  transition:right .5s cubic-bezier(0.645, 0.045, 0.355, 1);
}

.a {
  display:block;
  top:20%;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class='a'>
</div>
<div class='b'>
</div>
<button>Launch</button>

https://jsfiddle.net/qu2pybch/

yunzen
  • 32,854
  • 11
  • 73
  • 106