"Force Reflow" in CSS Transitions in Bootstrap

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! :)

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).

css transitions affecting previously modified css properties

You have to force a relayout after the first styles are applied (so they are processed without transitions) and then you can apply the styles that lead to a transition. The way you are doing it now, all the opeartions are being collapsed into one operation and thus everything is undergoing the transitions.

The simplest way to get the relayout is to apply the first CSS properties, then do a setTimeout(fn, 1) to apply the second set of properties in the timer callback. There are also other ways to force a relayout by requesting certain properties that trigger a relayout. I don't remember exactly which properties those are off the top of my head (would take some research).

I haven't tried this myself, but I think requesting a size property on your element such as .offsetHeight will force the relayout. The browser realizes that there are pending style changes and that those pending style changes might affect the size request so it does a relayout synchronously before returning the .offsetHeight value, thus solving your issue.

A somewhat similar question and answer: "Force Reflow" in CSS transitions in Bootstrap

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>

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/

Restart animation in CSS3: any better way than removing the element?

Just set the animation property via JavaScript to "none" and then set a timeout that changes the property to "", so it inherits from the CSS again.

Demo for Webkit here: http://jsfiddle.net/leaverou/xK6sa/
However, keep in mind that in real world usage, you should also include -moz- (at least).



Related Topics



Leave a reply



Submit