Why Does Reflow Need to Be Triggered for CSS Transitions

Why does reflow need to be triggered for CSS transitions?

(Effectively: "Why can't I easily use transitions with the display property")

Short Answer:

CSS Transitions rely on starting or static properties of an element. When an element is set to display: none; the document (DOM) is rendered as though the element doesn't exist. This means when it's set to display: block; - There are no starting values for it to transition.

Longer Answer:

  1. Reflow needs to be triggered because elements set to display: none; are not drawn in the document yet. This prevents transitions from having a starting value/initial state. Setting an element to display: none; makes the document render as if the element isn't there at all.
  2. He suggest reflowing because it's generally accepted to hide and show elements with display: none; and display: block; - typically after the element has been requested by an action (tab or button click, callback function, timeout function, etc.). Transitions are a huge bonus to UX, so reflowing is a relatively simple way to allow these transitions to occur. It doesn't have an enormous impact when you use simple transitions on simple sites, so for general purposes you can trigger a reflow, even if technically you shouldn't. Think of the guy's example like using unminified JavaScript files in a production site. Can you? Sure! Should you? Probably not, but for most cases, it won't make a hugely noticeable difference.
  3. There are different options available that prevent reflowing, or are generally easier to use than the method in the link you provided. Take the following snippet for a few examples:

A: This element is set to height: 0; and overflow: hidden;. When shown, it's set to height: auto;. We apply the animation to only the opacity. This gives us a similar effect, but we can transition it without a reflow because it's already rendered in the document and gives the transitions initial values to work with.

B: This element is the same as A, but sets the height to a defined size.

A and B work well enough for fading in elements, but because we set the height from auto/100px to 0 instantly, they appear to collapse on "fade out"

C: This element is hidden and we attempt to transition the child. You can see that this doesn't work either and requires a reflow to be triggered.

D: This element is hidden and we animate the child. Since the animation keyframes give a defined starting and ending value, this works much better. However note that the black box snaps into view because it's still attached to the parent.

E: This works similarly to D but we run everything off the child, which doesn't solve our "black box" issue we had with D.

F: This is probably the best of both worlds solution. We move the styling off the parent onto the child. We can trigger the animation off of the parent, and we can control the display property of the child and animate the transition as we want. The downside to this being you need use animation keyframes instead of transitions.

G: While I don't know if this triggers a reflow inside the function as I haven't parsed it myself, you can just simply use jQuery's .fadeToggle() function to accomplish all of this with a single line of JavaScript, and is used so often (or similar JS/jQuery fadeIn/fadeOut methods) that the subject of reflowing doesn't come up all that often.

Examples:

Here's a CodePen: https://codepen.io/xhynk/pen/gerPKq

Here's a Snippet:

jQuery(document).ready(function($){  $('button:not(#g)').click(function(){   $(this).next('div').toggleClass('show');  });
$('#g').click(function(){ $(this).next('div').stop().fadeToggle(2000); });});
* { box-sizing: border-box; }
button { text-align: center; width: 400px;}
div { margin-top: 20px; background: #000; color: #fff;}
.a,.b { overflow: hidden; height: 0; opacity: 0; transition: opacity 3s;}
.a.show { height: auto; opacity: 1;}.b.show { height: 100px; opacity: 1;}
.c,.d { display: none;}
.c.show,.d.show { display: block; }
.c div { opacity: 0; transition: 3s all;}
.c.show div { opacity: 1;}
.d div { opacity: 0;}
.d.show div { animation: fade 3s;}
@keyframes fade { from { opacity: 0; } to { opacity: 1; }}
.e div { display: none;}
.e.show div { display: block; animation: fade 3s;}
.f { background: transparent;}
.f div { background: #000; display: none;}
.f.show div { display: block; animation: fade 3s;}

.g { display: none;}
<button id="a">A: Box Height: Auto</button><div class="a">This<br/>Has<br/>Some Strange<br/><br/>Content<br>But<br>That doesn't really<br>Matter<br/>Because shown,<br/>I'll be<br/>AUTO</div><button id="b">B: Box Height: 100px</button><div class="b">Content For 2</div><button id="c">C: Hidden - Child Transitions (bad)</button><div class="c"><div>Content<br/>For<br/>3<br/></div></div><div style="clear: both;"></div><button id="d">D: Hidden - Child Animates (Better)</button><div class="d"><div>Content<br/>For<br/>4<br/></div></div><div style="clear: both;"></div><button id="e">E: Hidden - Child Hidden & Animates</button><div class="e"><div>Content<br/>For<br/>5<br/></div></div><button id="f">F: Child Has BG & Animates (Works)</button><div class="f"><div>Content<br/>For<br/>5<br/></div></div><div style="clear: both;"></div><button id="g">G: This uses fadeToggle to avoid this</button><div class="g">I animate with<br/>JavaScript</div><footer>I'm just the footer to show the bottom of the document.</footer><script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

CSS3 Transitions on reflow

I wouldn't think it's possible via CSS alone - transitions are not inheritable, so they would have to be applied to the nested elements in question, and a width transition couldn't be applied without the width anyway so e.g. the nested div would need it's width and 0 set to transition between them either on a hover or a JS click or some event

however I think I really am failing to understand the question;

#container div {transition: width 1s ease-in-out;}

would apply it to all child divs then you just toggle the display and width however you're thinking of doing it anyway?

Force browser to trigger reflow while changing CSS

Requesting the offsetHeight of an element does everything nicely. You can force a reflow using this function and passing it the element that styles have been changed on:

function reflow(elt){
console.log(elt.offsetHeight);
}

And call this where reflows are needed. See this example: http://jsfiddle.net/9WX5b/2/

EDIT: recently needed to do this, and wondered if there was a better way than to console.log it. You can't just write elt.offsetHeight as it's own statement, as the optimizer (Chrome's, at least) will not trigger a reflow because it is just accessing a property with no getter set, no need to even evaluate it. So, AFAIK the cheapest way to do this is void(elt.offsetHeight), as it does not know for sure if void has side effects or not. (could be overridden or something, idk).

Force Reflow in CSS transitions in Bootstrap

Bit of a late reply, but I'm tackling some issues with CSS transitions which I think relate to this bit of code you've found, and hopefully help you out with understanding it!

Basically, I'm toggling a class from Javascript / jQuery that adds css transitions to a dom element. The CSS of this element is then updated which causes the transition to occur. A simplified version of the code is below:

var myelement = $("myselector");

// Set z-indexes before the transition
myelement.css("z-index", 1);

var reflow = root.offset().left; // Re-flow the page

// Set the transition class on the element which will animate
myelement.addClass("trans");
myelement.css("width", 0 + "px"); // Animate to nothing

So if I uncomment my re-flow line, my transition will occur, but sometimes (it seems more often in safari) the z-index of myelement won't have been updated.

To me, it seems that in certain situations, the styles written to the dom are being buffered somewhere and not being flushed.

That's where the call to the left offset comes in. This is one of the properties that are said to cause a re-flow in the page. This is obviously usually a bad thing performance wise, but it seems necessary to prevent the css transitions picking up the wrong values.

There's an interesting Mozilla bug lodged which discusses the same subject. Might be of some interest. They suggest the addition of an API to properly start transitions from code.

This is also an interesting SO post about forcing re-flows.

Hope this helps! :)

CSS Transition is not being triggered as expected

You need to get rid of the display: none and let css take care of the transitions more.

I set the .np-collapsible to this in order to let the element always exist:

.np-collapsible:not(.np-expanded) {
height: 0;
overflow: hidden;
}

I set the transition class to not make any changes that would start up a transition (it only includes the transition property).

Then in the JS, the transition is done similarly to what you had originally, but the main difference is that I use menu.scrollHeight to get the height of the menu in order to avoid having extra transitions to get the height normally.

I also added the ability to contract the menu to the function. In case of contracting the menu, you have to remove np-expanded class before the transition due to the :not(.np-expanded) selector earlier stopping the overflow: none.

  // Retrieve the height of the menu
var targetHeight = menu.scrollHeight + 'px';
if (menu.classList.contains('np-expanded')) {
// It's already expanded, time to contract.
targetHeight = 0;
menu.classList.remove('np-expanded');
button.setAttribute('aria-expanded', false);
}
// Enable transition
menu.classList.add('np-transitioning');
menu.addEventListener('transitionend', function(event) {
// Disable transition
menu.classList.remove('np-transitioning');

// Indicate that the menu is now expanded
if (targetHeight) {
menu.classList.add('np-expanded');
button.setAttribute('aria-expanded', true);
}
}, {
once: true
});

// Set the height to execute the transition
menu.style.height = targetHeight;

Here's a working example:

var button = document.querySelector('.np-trigger');var menu = document.querySelector(button.dataset.target);
button.addEventListener('click', function(event) { expand();}, false);
function expand() { if (isTransitioning()) { // Don't do anything during a transition return; }
// Retrieve the height of the menu var targetHeight = menu.scrollHeight + 'px'; if (menu.classList.contains('np-expanded')) { // It's already expanded, time to contract. targetHeight = 0; menu.classList.remove('np-expanded'); button.setAttribute('aria-expanded', false); } // Enable transition menu.classList.add('np-transitioning'); menu.addEventListener('transitionend', function(event) { // Disable transition menu.classList.remove('np-transitioning');
// Indicate that the menu is now expanded if (targetHeight) { menu.classList.add('np-expanded'); button.setAttribute('aria-expanded', true); } }, { once: true });
// Set the height to execute the transition menu.style.height = targetHeight;}
function isTransitioning() { if (menu.classList.contains('np-transitioning')) { return true; }
return false;}
.np-collapsible:not(.np-expanded) {  height: 0;  overflow: hidden;}
.np-transitioning { transition: height 0.25s ease;}
.navigation-menu { display: flex; flex-direction: column; position: fixed; top: 4rem; left: 1rem; width: 270px;}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"><button class="btn btn-dark np-trigger" data-target="#menu">Menu</button>
<nav id="menu" class="bg-dark navigation-menu np-collapsible"> <a class="nav-link" href="#">Link</a> <a class="nav-link" href="#">Link</a> <a class="nav-link" href="#">Link</a></nav>

Using CSS Animation, if we need to animate it again, must be remove the class and add it back?

No, you don't need to remove it at that time, you can simply remove it just before setting it on again and trigger a reflow in-between:

const names = ["Peter", "Paul", "Mary"];
const displayElement = document.querySelector("#display");

function pickOne(arr) {
return arr[Math.floor(Math.random() * arr.length)];
}

let name = pickOne(names),
prevName = name;

displayElement.innerHTML = name;

setInterval(() => {
name = pickOne(names);
if (name !== prevName) {
displayElement.classList.remove("blur-and-back");
displayElement.offsetWidth; // trigger reflow
displayElement.classList.add("blur-and-back");

setTimeout(() => {
displayElement.innerHTML = name;
}, 1000);
prevName = name;
}

}, 3000);
#display {
font: 36px Arial, sans-serif;
}

.blur-and-back {
animation: 2s blur_and_back;
}

@keyframes blur_and_back {
0% {
filter: blur(0);
}
50% {
filter: blur(0.72em);
}
100% {
filter: blur(0);
}
}
<div id="display"></div>

Why don't I see layout/reflow being triggered when changing and retrieving element positions?

Checking the Element.style.property value will not trigger a reflow no. This value is not the "computed value", and this doesn't require the layout to be recalculated.

const target = document.querySelector(".target");

target.style.left = "0px";

// logs "0px"
// even though the real computed value is "25px"
// from the !important rule in CSS
console.log( target.style.left );

target.classList.remove("begin");
target.style.left = "250px";
// now the computed style is correctly 250px
// but since we didn't trigger a reflow
// the transition doesn't kick in
.target {
transition: left 2s;
width: 50px;
height: 50px;
background: salmon;
position: absolute;
}
.begin {
/* will take precedence over the .style rule */
left: 25px !important;
}
<div class="target begin"></div>


Related Topics



Leave a reply



Submit