I have 2 div
s, 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=".3.1/jquery.min.js"></script>
<div class='a'>A
</div>
<div class='b'>B
</div>
<button>Launch</button>
I have 2 div
s, 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./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
Share Improve this question edited Jan 24, 2019 at 13:26 yunzen 33.4k13 gold badges78 silver badges113 bronze badges asked Jan 24, 2019 at 10:56 RichardRichard 1,0381 gold badge9 silver badges33 bronze badges 5- normal behavior, you need a slight delay between the display and changing the right – Temani Afif Commented Jan 24, 2019 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 Commented Jan 24, 2019 at 11:50
- This is NOT a duplicate. How can I deny this? – Richard Commented Jan 24, 2019 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 e and better explain – Temani Afif Commented Jan 24, 2019 at 11:59
- check this: jsfiddle/r7016skg adding a small delay will output what you expect – Temani Afif Commented Jan 24, 2019 at 12:01
5 Answers
Reset to default 15To 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 plex, since only some DOM methods and the paint operation need it. It consists in updating all the CSS rules and recalculating all the puted styles of every elements on the page.
Being a quite plex 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 puted value of your element to create the initial state.
Since browsers do recalculate the puted 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 { putedStyle: {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 puted 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 puted values. For instance Element.getBoundingClientRect
, or element.offsetHeight
or getComputedStyle(element).height
etc. All these need the entire page to have updated puted 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./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./ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class='a'>A
</div>
<div class='b'>B
</div>
<button>Launch</button>
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 plex 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./ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class='a'>A
</div>
<div class='b'>B
</div>
<button>Launch</button>
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/789su6xb/
$('button').on('click',function(){
var b = $('.b');
b.show();
if(b.is(":visible")) {
$('.b').css('right','80%');
$('.a').css('right','80%');
}
});
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./ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class='a'>A
</div>
<div class='b'>B
</div>
<button>Launch</button>
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./ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class='a'>
</div>
<div class='b'>
</div>
<button>Launch</button>
https://jsfiddle/qu2pybch/