Posts Tagged ‘DOM render tree’

Triggering reflow to toggle a CSS3 transition

My previous post on triggering reflow focused on forcing reflow to allow for transitions when an element is put into the DOM render tree. This post doesn’t present any new concepts, but provides an example of triggering reflow, in the same manner, to allow for applying style changes to a DOM element instantly and without a transition, then turning the transition back on.

The changes to an element are typically:

  • Turn off the transition on the element
  • Update some properties of the element (that are part of the transition), and force reflow so that the changes are applied instantly without transition
  • Turn on the transition again

In working with transitions, this scenario has come up frequently for me, as much as dealing with DOM elements being put into the render tree.

One classic example where this becomes necessary is in constructing a circular carousel, as the trick to make it circular is to make the first slide a copy of the last slide – so the carousel starts on the second slide and when you reach the last slide, the carousel instantly jumps to the first slide and slides over to the second slide. This is all invisible to the user, and it appears that the carousel has transitioned smoothly from the last slide to the first slide.

The code for a working circular carousel with 3 slides (plus a copy of the last slide) is presented below, with the classes for the different states of the slides being slide1pre (copy of slide 3), slide1, slide2, and slide3.

Note the point where it is necessary to trigger reflow in order to invisibly jump to the first slide.

.carousel-viewport { width:70px; height:70px; overflow:hidden; }
.carousel-container { width:1000px; }

.carousel-container.notransition { transition:transform 0s; }
.carousel-container.dotransition { transition:transform 0.5s; }

.carousel-container.slide1pre { transform:translateX(0px); }
.carousel-container.slide1 { transform:translateX(-70px); }
.carousel-container.slide2 { transform:translateX(-140px); }
.carousel-container.slide3 { transform:translateX(-210px); }

.test-block { width:50px; height:50px; float:left; margin:10px; color:#fff; background:#222; }
<p><a class="ia-start-color-change" href="#">Rotate carousel</a></p>

<
div class="carousel-viewport">
<
div class="ia-carousel-container carousel-container dotransition slide1">
<
div class="ia-slide-pre test-block">3</div>
<
div class="ia-slide-1 test-block">1</div>
<
div class="ia-slide-2 test-block">2</div>
<
div class="ia-slide-3 test-block">3</div>
</
div>
</
div>
var curSlide = 1; // start at slide1

$('.ia-start-color-change').click(function (event) {

var testBlockElem = $('.ia-carousel-container');

// slide 1 to slide 2
if (curSlide == 1) {
testBlockElem.removeClass(
'slide1').addClass('slide2');
}

// slide 2 to slide 3
if (curSlide == 2) {
testBlockElem.removeClass(
'slide2').addClass('slide3');
}

// slide 3 to slide1pre, then slide1pre to slide1
if (curSlide == 3) {

// turn off transition
testBlockElem.removeClass('dotransition').addClass('notransition');
// go to slide1pre invisibly and instantly (w/o transition)
testBlockElem.removeClass('slide3').addClass('slide1pre');

//
// Force reflow to commit style changes to DOM
// allows us to re-apply transition rule, with the DOM updated to the carousel on slide1pre with no transition rule
//
testBlockElem[0].offsetWidth; // force reflow
//

// re-apply transition rule
testBlockElem.removeClass('notransition').addClass('dotransition');
// transition over to slide1
testBlockElem.removeClass('slide1pre').addClass('slide1');

// reset slide counter
// (note subsequent increment in this function, so we'll be at curSlide=1 when this function finishes)
curSlide = 0;
}

curSlide++;

event.preventDefault();
});

Triggering reflow for CSS3 transitions

There’s a lot written on minimizing reflow of the DOM, but one interesting situation is actually trying to force reflow in order to apply a CSS transition rule when the element is pushed into the DOM render tree (e.g. display:none to display:block, display:inline, etc.).

Problem

.test-block { width:50px; height:50px; background:#000; margin:10px; opacity:1; transition:opacity 0.5s; }
.test-block.invisible { opacity:0; }
.test-block.hide { display:none; }
<div class="ia-fadein-block test-block invisible hide"></div> $('.ia-fadein-block').removeClass('hide');
$(
'.ia-fadein-block').removeClass('invisible');

If we remove the hide and invisible classes from the div (as shown above), you’d assume that the div would transition smoothly to opacity:1 in 0.5s; however, this is not the case, the div will go to opacity:1 instantly.

What’s happening is that the removal of both classes occur prior to the browser adding the div to the DOM render tree; when the div gets rendered, it’s as if it never had the hide nor invisible class.

Reflow needs to be triggered before the invisible class is removed in order for the transition to work as expected.

Deprecated solution: setTimeout(func,0)

A setTimeout() with a delay of 0 was my go-to solution for the longest while. Here, the function argument specified would handle whatever needed to occur after the element was added to the DOM render tree (for the example presented above, this would mean the invisible class being removed in the function specified). The thinking behind using setTimeout() was that reflow would occur at some point in the current execution context and before the execution of the setTimeout() callback. This seemed true for a while, but with one of the Firefox releases last year (I don’t know specifically which) I noticed things breaking, with the Javascript engine executing the callback before reflow occured.

I did some quick hacks to fix existing code by increasing the time span before the timeout was triggered (typically 50ms-100ms), but this made a nasty hack even nastier and I wanted a more reliable solution.

Current solution: Trigger reflow manually

There are a number of calls that will trigger reflow on the DOM, but the most reliable calls, that always force reflow, seem to be the ones that return any sort of measurement that must be calculated (e.g. offsetWidth) on an element. With this in mind, the Javascript code can be modified as follows:

$('.ia-fadein-block').removeClass('hide');

// force reflow
$('.ia-fadein-block')[0].offsetWidth;
//

$('.ia-fadein-block').removeClass('invisible');

The code is for demonstration purposes; for repeated use, the hack to force reflow should really be encapsulated in a function, as there is no guarantee that the reflow behavior triggered by querying offsetWidth will remain consistent.

Ideal solution: API Method to trigger reflow or delay certain style changes post-reflow

The solution presented above is still a hack and may very well break in the future, as there is no guarantee that browsers won’t cache properties like offsetWidth in further attempts to minimize reflow. A robust, future-proof, solution is needed and it’s amazing one hasn’t come to fruition as yet.

A Mozilla bug report documents this issue and David Baron presents some workable solutions.