29

As the title implies, is there a proper way to set some initial CSS properties (or class) and tell the browser to transition these to another value?

For example (fiddle):

var el = document.querySelector('div'),
    st = el.style;
st.opacity = 0;
st.transition = 'opacity 2s';
st.opacity = 1;

This will not animate the opacity of the element in Chrome 29/Firefox 23. This is because (source):

[...] you’ll find that if you apply both sets of properties, one immediately after the other, then the browser tries to optimize the property changes, ignoring your initial properties and preventing a transition. Behind the scenes, browsers batch up property changes before painting which, while usually speeding up rendering, can sometimes have adverse affects.

The solution is to force a redraw between applying the two sets of properties. A simple method of doing this is just by accessing a DOM element’s offsetHeight property [...]

In fact, the hack does work in the current Chrome/Firefox versions. Updated code (fiddle - click Run after opening the fiddle to run animation again):

var el = document.querySelector('div'),
    st = el.style;
st.opacity = 0;
el.offsetHeight; //force a redraw
st.transition = 'opacity 2s';
st.opacity = 1;

However, this is rather hackish and is reported to not work on some android devices.

Another answer suggests using setTimeout so the browser has time to perform a redraw, but it also fails in that we don't know how long it will take for a redraw to take place. Guessing a decent number of milliseconds (30-100?) to ensure that a redraw occurred means sacrificing performance, unnecessarily idling in the hopes that the browser performs some magic in that little while.

Through testing, I've found yet another solution which has been working great on latest Chrome, using requestAnimationFrame (fiddle):

var el = document.querySelector('div'),
    st = el.style;
st.opacity = 0;
requestAnimationFrame(function() {
    st.transition = 'opacity 2s';
    st.opacity = 1;
});

I assume that requestAnimationFrame waits until right before the beginning of the next repaint before executing the callback, hence the browser does not batch up the property changes. Not entirely sure here, but works nicely on Chrome 29.

Update: after further testing, the requestAnimationFrame method does not work very well on Firefox 23 - it seems to fail most of the time. (fiddle)

Is there a proper or recommended (cross-browser) way of achieving this?

Community
  • 1
  • 1
Fabrício Matté
  • 69,329
  • 26
  • 129
  • 166
  • 1
    I believe the cleanest way is to just add and remove classes, instead of dealing with the properties directly. But this is not pure js, as the properties and values being changed are in the CSS (you can manipulate the stylesheets with js, but it's kinda ugly too). – bfavaretto Sep 02 '13 at 03:09
  • 2
    @bfavaretto Well, the same behavior happens when I add/remove classes from the element. Moving the `opacity:0` and `opacity:1` to CSS rules then adding the given classes result in the same outcome from my testing. Here's the [fiddle](http://jsfiddle.net/Jhuy6/7/) to prove that. I'm actually using classes in my actual use case, but the result is the same independently of dealing with properties or classes so far. – Fabrício Matté Sep 02 '13 at 03:11
  • Huh, `setTimeout` works for me. And according to [MDN](https://developer.mozilla.org/en-US/docs/Web/API/window.requestAnimationFrame), `requestAnimationFrame` might be better suit your require, as it guarantee to run your code on the next redraw. – Passerby Sep 02 '13 at 03:18
  • @Passerby Yes, I'm tending towards `requestAnimationFrame`, though the wording from MDN "call a specified function to update an animation before the next repaint" sounds like the property changes could be batched up before the next repaint is ensued, though it does work. I'm probably misinterpreting that statement, will have to check the specs. – Fabrício Matté Sep 02 '13 at 03:23
  • I didn't realize that. Another thing I see is that you are applying the `transition` property asynchronously. Shouldn't it be applied synchronously? – bfavaretto Sep 02 '13 at 03:24
  • @bfavaretto That doesn't make a difference I believe. As long as you set the `transition` property after the initial or before the final property value it will be applied equally, the problem is setting the initial and final transitioned property values in a single redraw as then the initial value is ignored. – Fabrício Matté Sep 02 '13 at 03:29
  • 1
    @FabrícioMatté I think the wording just mean that the callback will be executed upon next repaint. As long as it's separated from the "current" queue, they won't be batched up (that's why I believe `setTimeout` should work too). And in your fiddle case, setting transition inside and outside `requestAnimationFrame`/`setTimeout` does make a difference: element by default has `opacity:1`, so setting `opacity:0` _and_ `transition` outside makes it _immediately_ begin fading, and then in the delayed callback, transit back to 1. Thus putting `transition` inside callback is crucial in your fiddle. – Passerby Sep 02 '13 at 03:40
  • Oh you're absolutely right, if I don't set the transition inside the callback the browser will execute the animation from the default value to the one that I've set in the same frame as the `transition` property, nice pick. `=]` – Fabrício Matté Sep 02 '13 at 03:58
  • Just noticed that my `requestAnimationFrame` is broken on Firefox 23 (stable) - Nightly 26. – Fabrício Matté Sep 02 '13 at 05:08
  • have you used any js framework in the past? Like TweenMax(http://www.greensock.com/tweenmax/), try this... – SaurabhLP Sep 02 '13 at 05:52
  • @SaurabhLP Thanks for the link, I'll check it more throughout when I have a bit of free time. Though for this question I'd like to stay closer to the vanilla JS/CSS so that it may be useful for future readers. – Fabrício Matté Sep 02 '13 at 06:41
  • `setTimeout` is *not* supposed to work: https://bugzilla.mozilla.org/show_bug.cgi?id=701626 – Nickolay Aug 06 '15 at 16:32

5 Answers5

15

There isn't a clean way at this moment (without using CSS Animations -- see the nearby answer by James Dinsdale for an example using CSS Animations). There is a spec bug 14617, which unfortunately wasn't acted upon since it was filed in 2011.

setTimeout does not work reliably in Firefox (this is by design).

I'm not sure about requestAnimationFrame -- an edit to the original question says it doesn't work reliably either, but I did not investigate. (Update: it looks like requestAnimationFrame is considered at least by one Firefox core developer to be the place where you can make more changes, not necessarily see the effect of the previous changes.)

Forcing reflow (e.g. by accessing offsetHeight) is a possible solution, but for transitions to work it should be enough to force restyle (i.e. getComputedStyle): https://timtaubert.de/blog/2012/09/css-transitions-for-dynamically-created-dom-elements/

window.getComputedStyle(elem).opacity;

Note that just running getComputedStyle(elem) is not enough, since it's computed lazily. I believe it doesn't matter which property you ask from the getComputedStyle, the restyle will still happen. Note that asking for geometry-related properties might cause a more expensive reflow.

More information on reflow/restyle/repaint: http://www.phpied.com/rendering-repaint-reflowrelayout-restyle/

Nickolay
  • 31,095
  • 13
  • 107
  • 185
  • The property asked DOES it matters. It must be the one used on the CSS transition. If not this trick won't work on Chrome or Edge. However it works fine on Firefox regardless of the property accessed. – AxeEffect Nov 05 '15 at 19:06
10

The situation has changed since 2013, so here's a new answer:

You could use Web Animations. They are implemented natively in Chrome 36 and Firefox 40 and there is a polyfill for all the other browsers.

Example code:

var player = snowFlake.animate([
  {transform: 'translate(' + snowLeft + 'px, -100%)'},
  {transform: 'translate(' + snowLeft + 'px, ' + window.innerHeight + 'px)'}
], 1500);

// less than 1500ms later...changed my mind
player.cancel();
mik01aj
  • 11,928
  • 15
  • 76
  • 119
  • 3
    Since version 40 (also mobile) web animations are also supported by Firefox: https://developer.mozilla.org/en-US/docs/Web/API/Animation. – AxeEffect Nov 10 '15 at 11:37
  • ...but browser support is still insufficient for use in production in 2017, see https://caniuse.com/#search=web%20animations – schellmax Dec 01 '17 at 15:58
3

You shouldn't need too much JavaScript to achieve what you want, just use CSS keyframes and animations for the same effect.

div {
    opacity: 0;
}

div.fadeIn {
    -webkit-animation: fadeIn 2s forwards;
    animation: fadeIn 2s forwards;
}

@keyframes fadeIn {
    0% {opacity: 0;}
    100% {opacity: 1;}
}

@-webkit-keyframes fadeIn {
    0% {opacity: 0;}
    100% {opacity: 1;}
}

As shown in this JsFiddle it works either from page load (with class already added) or when dynamically adding the class to trigger the animation.

James Dinsdale
  • 789
  • 8
  • 23
0

  1.   <script  src="http://cdnjs.cloudflare.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script>
       <script>
    
         $(document).ready(function(){
    $('a.hiw*').click(function(){ 
     
     id = this.id;
     dval = $('#'+id).attr('data-value');
     if (dval == 0) {
      $('a.hiw*').attr('data-value','0');
      $( ".hiw-popup" ).remove();
      $('#'+id).attr('data-value','1');
      $('<div class="hiw-popup white-well run-animation hidden-xs"><div class="row text-center"><div class="col-sm-2 animation-hiw col-xs-2"><span class="glyphicon glyphicon-pencil hiw-icon1" style="background:#ffffff;">1</span><br/>block1</div><div class="col-sm-2 animation-hiw col-xs-2"><span class="glyphicon glyphicon-shopping-cart hiw-icon2">2</span><br/>BLOCK3</div><div class="col-sm-3 animation-hiw col-xs-3"><span class="glyphicon glyphicon-folder-open hiw-icon3">3</span><br/>BLOCK2</div><div class="col-sm-3 animation-hiw col-xs-3"><span class="glyphicon glyphicon-ok hiw-icon4">4</span><br/>BLOCK</div><div class="col-sm-2 animation-hiw col-xs-2"><span class="glyphicon glyphicon-arrow-down hiw-icon5">5</span><br/>BLOCK</div></div></div>').insertAfter('#'+id);
     }else{
      $('#'+id).attr('data-value','0');
      $( ".hiw-popup" ).remove();
     } 
    });
    });
    var ahiw = function(id){ 
     dval = $('#'+id).attr('data-value');
     if (dval == 0) {
      $('a.hiw*').attr('data-value','0');
      $( ".hiw-popup" ).remove();
      $('#'+id).attr('data-value','1');
      $('<div class="hiw-popup white-well run-animation hidden-xs"><div class="row text-center"><div class="col-sm-2 animation-hiw col-xs-2"><span class="glyphicon glyphicon-pencil hiw-icon1" style="background:#ffffff;">1</span><br/>block1</div><div class="col-sm-2 animation-hiw col-xs-2"><span class="glyphicon glyphicon-shopping-cart hiw-icon2">2</span><br/>BLOCK3</div><div class="col-sm-3 animation-hiw col-xs-3"><span class="glyphicon glyphicon-folder-open hiw-icon3">3</span><br/>BLOCK2</div><div class="col-sm-3 animation-hiw col-xs-3"><span class="glyphicon glyphicon-ok hiw-icon4">4</span><br/>BLOCK</div><div class="col-sm-2 animation-hiw col-xs-2"><span class="glyphicon glyphicon-arrow-down hiw-icon5">5</span><br/>BLOCK</div></div></div>').insertAfter('#'+id);
     }else{
      $('#'+id).attr('data-value','0');
      $( ".hiw-popup" ).remove();
     }
    }
       
    </script>
    /* Chrome, Safari, Opera */
    @-webkit-keyframes animation-hiw-icon {
      from {
        background-color: #d9d9d9;
      }
      to {
        background-color: #4ad18f;
      }
    }
    /* Standard syntax */
    @keyframes animation-hiw-icon {
      from {
        background-color: #d9d9d9;
      }
      to {
        background-color: #4ad18f;
      }
    }
    
    
    /* Chrome, Safari, Opera */
    @-webkit-keyframes animation-hiw-prog {
      from {
       background-color: #d9d9d9;
        width: 0%
      }
      to {
        width: 100%;
      background-color: #4ad18f;
      }
    }
    /* Standard syntax */
    @keyframes animation-hiw-prog {
      from {
        width: 0%
      }
      to {
        width: 100%;
      }
    }
    /* Chrome, Safari, Opera */
    @-webkit-keyframes animation-hiw-pop {
      from {
        opacity: 0.5;
     background-color: #d9d9d9;
        -ms-transform: scale(0.8); /* IE 9 */
        -webkit-transform: scale(0.8); /* Chrome, Safari, Opera */
        transform: scale(0.8);
      }
      to {
       background-color: #4ad18f;
        opacity: 1;
        font-weight: normal;
        -ms-transform: scale(.8); /* IE 9 */
        -webkit-transform: scale(.8); /* Chrome, Safari, Opera */
        transform: scale(.8);
      }
    }
    /* Standard syntax */
    @keyframes animation-hiw-pop {
      from {
      background-color: #d9d9d9;
        opacity: 0.5;
        -ms-transform: scale(0.8); /* IE 9 */
        -webkit-transform: scale(0.8); /* Chrome, Safari, Opera */
        transform: scale(0.8);
      }
      to {
      background-color: #4ad18f;
        opacity: 1;
        font-weight: normal;
        -ms-transform: scale(.8); /* IE 9 */
        -webkit-transform: scale(.8); /* Chrome, Safari, Opera */
        transform: scale(.8);
      }
    }
    /*Animation Trigger*/
    .run-animation .hiw-progress:after, .run-animation .animation-hiw, .run-animation .hiw-icon1, .run-animation .hiw-icon2, .run-animation .hiw-icon3, .run-animation .hiw-icon4, .run-animation .hiw-icon5 {
      -webkit-animation-play-state: running; /* Safari and Chrome */ 
      animation-play-state: running;
    }
    
    
    .run-animation .hiw-progress:after, .run-animation .animation-hiw, .run-animation .hiw-icon1, .run-animation .hiw-icon2, .run-animation .hiw-icon3, .run-animation .hiw-icon4, .run-animation .hiw-icon5 {
      -webkit-animation-play-state: running;
      animation-play-state: running;
    }
    .hiw-progress:after {
      content: "";
      width: 0%;
      height: 5px;
      background: #4ad18f;
      display: inline-block;
      position: absolute;
      top: 0;
      left: 0;
      -webkit-animation: animation-hiw-prog 5s linear forwards;
      animation: animation-hiw-prog 5s linear forwards;
      -webkit-animation-play-state: paused;
      animation-play-state: paused;
    }
    .white-well {
      background-color: #fff;
      padding: 10px 15px;  border-radius: 5px;
      border: 1px solid #f1f1f1;
    }
    .hiw-popup {
      position: absolute;
       width: 100%;
      z-index: 9;
      margin: 30px 0 0 -15px;
      padding: 0px 15px !important;
      border-color: rgba(0, 0, 0, 0.25) !important;
      box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.60);
      -webkit-box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.60);
      -mz-box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.60);
    }
    .hiw-popup .arrow {
      position: absolute;
      display: block;
      width: 0;
      height: 0;  border-color: transparent;
      border-style: solid;
      border-width: 11px;
      left:90%;
      margin-left: -11px;
      border-top-width: 0;
      border-bottom-color: rgba(0, 0, 0, 0.25);
      top: -11px;
    }
    .hiw-popup .glyphicon {
      margin-bottom: 10px;
      margin-right: 0px !important;font-weight:bold;
      background-color: #ffffff;color:#222222 !important;
    }
    .white-well .glyphicon {
       background-color: #ffffff!important;
      border-radius: 76px;margin-top: -3px;color:#d9d9d9 !important;
      padding: 5px 9px 8px;
      color: #fff;
      box-shadow: 0px 0px 3px #222222;
      border: 3px solid #ffffff;
    }
    .glyphicon {
      position: relative;
      top: 1px;
      display: inline-block;
      font-family: 'Glyphicons Halflings';
      font-style: normal;
      font-weight: normal;
      line-height: 1;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
    }
    .clearfix:before, .clearfix:after, .container:before, .container:after, .container-fluid:before, .container-fluid:after, .row:before, .row:after, .form-horizontal .form-group:before, .form-horizontal .form-group:after, .btn-toolbar:before, .btn-toolbar:after, .btn-group-vertical > .btn-group:before, .btn-group-vertical > .btn-group:after, .nav:before, .nav:after, .navbar:before, .navbar:after, .navbar-header:before, .navbar-header:after, .navbar-collapse:before, .navbar-collapse:after, .modal-footer:before, .modal-footer:after, .review:before, .review:after, .panel-body:before, .panel-body:after {
      content: " ";
      display: table;
    }
    .animation-hiw:nth-child(1) {
      -webkit-animation-delay: .2s;
      animation-delay: .2s;
    }
    
    .hiw-icon5 {
      -webkit-animation: animation-hiw-icon 0.2s forwards;
      animation: animation-hiw-icon 0.2s forwards;
      -webkit-animation-delay: 5s;
      animation-delay: 5s;
      -webkit-animation-play-state: paused;
      animation-play-state: paused;
    }
    .hiw-icon4 {
      -webkit-animation: animation-hiw-icon 0.2s forwards;
      animation: animation-hiw-icon 0.2s forwards;
      -webkit-animation-delay: 3.75s;
      animation-delay: 3.75s;
      -webkit-animation-play-state: paused;
      animation-play-state: paused;
    }
    .hiw-icon3 {
      -webkit-animation: animation-hiw-icon 0.2s forwards;
      animation: animation-hiw-icon 0.2s forwards;
      -webkit-animation-delay: 2.25s;
      animation-delay: 2.25s;
      -webkit-animation-play-state: paused;
      animation-play-state: paused;
    }
    .hiw-icon2 {
      -webkit-animation: animation-hiw-icon 0.2s forwards;
      animation: animation-hiw-icon 0.2s forwards;
      -webkit-animation-delay: 1s;
      animation-delay: 1s;
      -webkit-animation-play-state: paused;
      animation-play-state: paused;
    }
    .hiw-icon1 {
      -webkit-animation: animation-hiw-icon 0.2s forwards;
      animation: animation-hiw-icon 0.2s forwards;
      -webkit-animation-delay: .2s;
      animation-delay: .2s;
      -webkit-animation-play-state: paused;
      animation-play-state: paused;
    }
    
    .animation-hiw {
      -webkit-animation: animation-hiw-pop 0.2s forwards; /* Chrome, Safari, Opera */
      animation: animation-hiw-pop 0.2s forwards;
      -webkit-animation-play-state: paused; /* Safari and Chrome */
      animation-play-state: paused;
      opacity: 0.5;
      -ms-transform: scale(0.8); /* IE 9 */
      -webkit-transform: scale(0.8); /* Chrome, Safari, Opera */
      transform: scale(0.8);
        background: #d9d9d9;
       width: 15%;
      padding: 2% 1%;
      height: 140px;
      color: #ffffff;  float: left;
    }
    .animation-hiw:nth-child(1){ -webkit-animation-delay: .2s; animation-delay: .2s; }
    .animation-hiw:nth-child(2){ -webkit-animation-delay: 1s; animation-delay: 1s; }
    .animation-hiw:nth-child(3){ -webkit-animation-delay: 2.25s; animation-delay: 2.25s; }
    .animation-hiw:nth-child(4){ -webkit-animation-delay: 3.75s; animation-delay: 3.75s; }
    .animation-hiw:nth-child(5){ -webkit-animation-delay: 5s; animation-delay: 5s; }
    
    hiw {
      visibility: hidden;
      font-size: 12px;
      font-style: italic;
      text-align: right;
      float: right;
    }
    <body>
    <a href="javascript:void(0);" class="hiw hidden-xs" id="hiw_1" data-value="1" style="float:LEFT;margin-right:10px;color: #4ad18f;font-size: 12px;padding:0px 0px 5px 0px;">How it works</a>
    </body>

    #

-1

Here's a working version. See it yourself.

Tested on Chrome, Firefox, Opera.

On my version of firefox, it does not support style.transition so I made it to fallback to vendor specific name if standard name is not available.

http://jsfiddle.net/eNCBz/5/

var el = document.querySelector('div');

var VENDORS = ['Moz', 'Webkit', 'Ms', 'O'];

function getVendorSpecificName(el, prop) {
    var style = el.style;
    if (prop in style) {
        return prop;
    }
    prop = ucfirst(prop);
    for (var i = 0, l = VENDORS.length, name; i < l; i++) {
        name = VENDORS[i] + prop;
        if (name in style) {
            return name;
        }
    }
    return null;
}

function ucfirst(str) {
    return str && str.charAt(0).toUpperCase() + str.substring(1);
}

function toCamelCase(str) {
    return str.split('-').map(function (str, i) {
        return i > 0 ? ucfirst(str) : str;
    }).join('');
}

function animateCss(el, prop, from, to, duration) {
    var style = el.style,
        camel = toCamelCase(prop),
        vendorSpecific = getVendorSpecificName(el, camel);
    if (!vendorSpecific) {
        console.log(prop + ' is not supported by this browser');
        return false;
    }

    var transitionPropName = getVendorSpecificName(el, 'transition');
    if (!(transitionPropName in style)) {
        console.log('transition is not supported by this browser');
        return false;
    }

    style[vendorSpecific] = from;

    setTimeout(function () {
        style[transitionPropName] = prop + ' ' + duration + 's ease';
        setTimeout(function () {
            style[vendorSpecific] = to;
        }, 1);
    }, 1);
    return true;
}

animateCss(el, 'opacity', 0, 1, 2);

Let me explain what is going on:

  • made some helper functions like ucfirst, toCamelCase

    • style property uses camel case names
  • try to find vendor specific style property name if standard name is not available

  • utilize setTimeout function to make sure browser to redraw

    • as far as I know, all browsers always redraw on timeout

I tried to make it more generic function so other properties can be applied as well, such as color or background.

Hope this helps!

Joon
  • 9,346
  • 8
  • 48
  • 75
  • Thank you for the input. I've made some modifications so that it can animate the same element multiple times: http://jsfiddle.net/eNCBz/7/ Though there seems to be a problem with very small timeouts, Firefox fails to redraw rather often (and Chrome also fails sometimes when the page/elements being transitioned are more complex). I think 100ms would be enough, but even then not a very clean solution (asynchronously hoping for browser magic). – Fabrício Matté Sep 03 '13 at 16:44