Search code examples
csscss-transitionsreflow

Why does a transition occur when showing an element after setting a property while the element is hidden?


A live example can be seen here.

A red square (showing) is directly above a green square (hidden as overflow). Click on the square, and both colored squares are instantly made fully transparent. Additionally, the height of the red square is set to 0; this fires a transition, but the transition goes unseen because the red square is now transparent.

Before clicking the square again, examine the toggle function. Looking at the JavaScript, I would expect the height of the red square to be reset to its original value without firing a transition. The transition should be suppressed, because the transition property is temporarily set to none while the height is changed.

Now click on the square again. Both colored squares are instantly made fully opaque, but the red square slides down as its height transitions from 0 to the original value. It's as though the height set by the inline style wasn't removed by the toggle function until the element was visible, and by then the transition property had also been reset.

Triggering a reflow seems to force the change in height to be applied. (Uncomment the line containing offsetParent to test.) This behavior occurs across browsers (at least Chrome and Safari, Firefox, and Opera), so I'm wondering if it isn't part of some spec. I've checked the CSS Transitions Module without success. Any ideas why this behavior occurs, and why it's so consistent across implementations?


Solution

  • This really is a bizarre problem. I don't think you're doing anything wrong in your code—the current browser implementations are just buggy.

    I've run into these kind of seemingly-obvious bugs before with CSS transitions, and they're a huge pain to deal with without resorting to byzantine hacks that are sure to break once the bug they're working around is fixed (in this case, my WebKit fix).

    I really dug into this, but couldn't come up with a clean solution that worked in the three main transitions-supporting layout engines (WebKit, Gecko, and Presto). That said, here's what I did figure out—hopefully someone smarter than me (or just coming at this with fresh eyes) can take this answer and turn it into true solution.

    Gecko and Presto (but not WebKit!)

    It looks like (and I am not a browser engineer or familiar with the spec) that any current or previous value of a transition-property will continue to be rendered regardless of whether or not it needs to be. So even though you've changed the value of transition-property, the browser is still rendering the height transition in the background, and when you change the height back, you get the trailing end of that.

    There is a solution though: create the transition in JavaScript (don't put it anywhere in the style sheet), remove it (after which there are no transition rules applied to #upper anywhere in the DOM), change the height, and then re-add it. Not perfect, but not a bug-reliant hack either.

    http://jsfiddle.net/grantheaslip/e3quW/

    JavaScript

    upper.style.removeProperty('transition');
    upper.style.removeProperty('-o-transition');
    upper.style.removeProperty('-moz-transition');
    upper.style.removeProperty('-webkit-transition');
    upper.style.removeProperty('height');
    // force a reflow
    // if (upper.offsetParent) { /* empty */ }
    upper.style['transition'] = 'height 1000ms';
    upper.style['-o-transition'] = 'height 1000ms';
    upper.style['-moz-transition'] = 'height 1000ms';
    upper.style['-webkit-transition'] = 'height 1000ms';
    

    Style sheet

    #upper {
        background-color: red;
    }
    

    WebKit (but not Gecko or Presto!)

    Anything that only works because of a 1ms timeout probably should never go anywhere near production, but I think this is worth pointing out in case it helps someone get to the bottom of this problem.

    My guess is that WebKit doesn't have the same issue as Presto or Gecko, but instead includes an optimization that gathers style changes applied in the same function and applies them all at once. Again, pure speculation from someone who's never gone near the WebKit source or CSS3 spec.

    http://jsfiddle.net/grantheaslip/DFcg9/

    JavaScript

    window.setTimeout(function() {
        upper.style.removeProperty('transition-property');
        upper.style.removeProperty('-o-transition-property');
        upper.style.removeProperty('-moz-transition-property');
        upper.style.removeProperty('-webkit-transition-property');
        upper.style.removeProperty('opacity');
        lower.style.removeProperty('opacity');
    }, 1);
    

    Gecko, Presto, and WebKit

    Here's both solutions combined. Again, because of the timeout hack, this really shouldn't be used.

    http://jsfiddle.net/grantheaslip/N3NrB/