5

I have a stylesheet which sets a css transition property like so (prefixed versions omitted for brevity):

transition: opacity 1s;

Then I have a number of elements on the page, and I wish to modify the transition-delay property of each element via JavaScript, to give a stagger effect. I am using jQuery like so:

$(element).css('transition-delay', delay + 's');

However, the above code does not add an inline transition-delay: Xs to the element. Instead, it results in:

<div style="transition: Xs;">

But that's fine, because it works as expected. Somehow, the browser knows that transition: Xs really means to just set the transition-delay to Xs and leave the rest intact.

However:

If I now get the inline style of that element via $(element).attr('style'), and then re-apply it to the element, $(element).attr('style', style), the HTML looks exactly the same, but now the transition has totally overwritten the other properties and essentially sets the element's transition value to all Xs ease 0s.

// HTML before - working
<div style="transition: Xs">

// then I do this
var style = $(el).attr('style');
$(el).attr('style', style);

// HTML after - broken!
<div style="transition: Xs">

Demo

A JSFiddle of exactly what I have described: http://jsfiddle.net/7vp8m/4/

What is going on?

Community
  • 1
  • 1
Michael Bromley
  • 4,792
  • 4
  • 35
  • 57
  • I'm not seeing a
    propping up in your moving elements. If there a different element on the page you are targeting and I'm missing something?
    – TheHamstring Jul 24 '14 at 20:47
  • Well the inline style attribute is set dynamically by the JavaScript, so it's not there in the initial HTML, if that's what you mean. If you right click a red box and inspect it in chrome dev tools for example, you should see the inline style declaration (it'll also have opacity and margin-left set in addition to the transition, due to the animation running) – Michael Bromley Jul 24 '14 at 20:58
  • Interesting! Seems to be a browser issue since the problem appears even when setting the style with Vanilla JS. – polarblau Jul 24 '14 at 21:19
  • 1
    As a work–around you can read–out the transition properties for each element, build a shorthand string and then set that: http://jsfiddle.net/7vp8m/5/ — Doesn’t explain what’s going on, though. Seems to me like the browsers might not support this style key. – polarblau Jul 24 '14 at 21:29
  • Why do you need to set `style = style`? – Zach Saucier Jul 25 '14 at 03:04
  • @ZachSaucier I am doing some JavaScript animations, and need to reset the position of the div (which is defined by inline CSS) at the end. – Michael Bromley Jul 25 '14 at 05:04

2 Answers2

5

I think just writing out the question and coding that demo really helped me to find the answer:

The HTML style attribute is not the actual style. We need to use the CSSStyleDeclaration object

Although it seems that the inline style is as simple as whatever is contained in the style="..." HTML attribute (as I had assumed), it turns out that this is not the case. Behind the scenes, inline styles (and all other styles) are actually defined by an object called CSSStyleDeclaration. The string contained in the style attribute only represents this object, but does not contain all the information needed to define a style.

This is why setting `el.style = "width: 100px;" does not work. From the MDN article on HTMLElement.style:

Except in Opera, styles can not be set by assigning a string to the (read only) style property, as in elt.style = "color: blue;". This is because the style attribute returns a CSSStyleDeclaration object. Instead, you can set style properties like this:

elt.style.color = "blue";  // Directly

var st = elt.style;
st.color = "blue";  // Indirectly

So this shows us why doing $(el).attr('style', 'transition: Xs'); will not work as expected - and this is exactly what I was running into. Doing so will modify the underlying CSSStyleDeclaration object, but not always in the way we want it to (hence my original question).

The solution is therefore to use the API provided by CSSStyleDeclaration. The following SO question proved crucial to me understanding this issue: JavaScript & copy style

Copying a CSSStyleDeclaration:

var originalStyle = el.cloneNode().style;

Here we are using the cloneNode() method because otherwise (if we just get el.style) the CSSStyleDeclaration object is copied by reference, which is not what I want since I will be changing the element's inline style and then I want to restore the original style later. Cloning the element first allows us to get a "fresh" copy of the CSSStleDeclaration that will not change when we alter the inline styles of our element el.

Replacing current inline style with the saved CSSStyleDeclaration

// first we need to delete all the style rules
// currently defined on the element
for (var i = el.style.length; i > 0; i--) {
    var name = el.style[i];
    el.style.removeProperty(name);
}

// now we loop through the original CSSStyleDeclaration 
// object and set each property to its original value

for (var i = originalStyle.length; i > 0; i--) {
    var name = originalStyle[i];
    el.style.setProperty(name,
        originalStyle.getPropertyValue(name),
        priority = originalStyle.getPropertyPriority(name));
}

Demo

Here is an update of my original demo that implements the above methods: http://jsfiddle.net/7vp8m/11/

Community
  • 1
  • 1
Michael Bromley
  • 4,792
  • 4
  • 35
  • 57
0

It breaks in chrome and the "new" opera but no in ff. In Maxthon it stops and restarts the animation the first time, then it works well.

As you said in http://jsfiddle.net/7vp8m/5 (fortunately you solved it) it is due to setting the transition delays through a inline style.

But if you force the engine to refresh the css it works somehow (stops the animation at first but then it continues, playing the animation slower): http://jsfiddle.net/7vp8m/7/

function tick() {
    [...]
    $.each($('.test'), function(i, e){
        e.style.marginLeft = x + 'px'; // Trying with vanilla js but it is the same
        e.offsetHeight; // force the refresh. It moves again but bad
    });
    [...]
}

This doesn't work too: http://jsfiddle.net/7vp8m/8/

$.each($('.test'), function (index, el) {
    var style = $(el).attr('style');
    style += '; transition-delay: '+delay + 's;'+
             '-webkit-transition-delay'+delay + 's;'
    $(el).attr('style', style);
    delay += 0.2;
});

It seems to be a webkit bug related to transition-delay, but Maxthon stopped the animation in a similar way so it probably is a more generalized bug.

So, if it is a bug, the best option is to not use the property transition-delay through js.

Forestrf
  • 364
  • 7
  • 13
  • 1
    Hi, thanks for looking into it. Turns out it is *not* a browser bug, but is down to the fact that I was ignorant of the way style definitions work behind the scenes. See my answer for an in-depth explanation. – Michael Bromley Jul 25 '14 at 05:55
  • Actually, having looked further into it, the particular weirdness with `transition` seems to be limited to Chrome. FF and IE handle it as expected. – Michael Bromley Jul 26 '14 at 08:54