10

Let's say I have a style like this, with a transition:

#theElement {
    position: relative;
    left: 200px;
    transition: left 1s;
}

And some code:

var element = document.getElementById("theElement");

function animateX(px) {
    element.style.left = px + "px";
}

So, simply there is a animateX function, which simply says what it does, animates the left propery of theElement, now what if I also want to have a function that instantly sets the left property, without a transition:

function setX(px) {
    element.style.transition = "none";
    element.style.left = px + "px";
    element.style.transition = "left 1s";
}

Why doesn't this work and how do I fix it?

Josh Crozier
  • 233,099
  • 56
  • 391
  • 304

4 Answers4

13

In order to make this work, you need to flush the CSS changes by reading them in the JS. The answer to this question explain how this works:

Can I use javascript to force the browser to "flush" any pending layout changes?

Working example below:

var element = document.getElementById("theElement");

function animateX(px) {
    element.style.left = px + "px";
}

function setX(px) {
    element.style.transition = "none";
    element.style.left = px + "px";
    // apply the "transition: none" and "left: Xpx" rule immediately
    flushCss(element);
    // restore animation
    element.style.transition = "";
}

function flushCss(element) {
  // By reading the offsetHeight property, we are forcing
  // the browser to flush the pending CSS changes (which it
  // does to ensure the value obtained is accurate).
  element.offsetHeight;
}
#theElement {
  position: relative;
  left: 200px;
  transition: left 1s;
  width: 50px;
  height: 50px;
  background: red;
}
.wrapper {
  width: 400px;
  height: 100px;
}
<div class="wrapper">
  <div id="theElement"></div>
</div>
<button onclick="animateX(100)">Animate 100</button>
<button onclick="setX(0)">Set 0</button>
Community
  • 1
  • 1
jperezov
  • 3,041
  • 1
  • 20
  • 36
  • Do you really need to flush two times, cause lex82's answer only needs one timeout? –  Jan 11 '16 at 16:23
  • No. I thought it'd be easier to understand by showing the flush twice, but I suppose it just makes it more confusing. I've edited my answer to demonstrate this. – jperezov Jan 11 '16 at 16:26
  • As explained in my answer, you should not restore the animation by setting it explicitly on the element. You will override the stylesheet (like you did when suppressing the animation). – lex82 Jan 11 '16 at 16:28
  • Good answer. My version of this would have been to do the second half of it in a setTimeout, but this is better. – Katana314 Jan 11 '16 at 16:36
  • Really interesting way of forcing properties to be applied using "offsetHeight" - thanks for the insight! – Nico Knoll Jan 02 '21 at 10:49
6

The other solutions are good, but I wanted to provide an alternative and explain what is happening.

The reason it's not working as expected in your example is because all of the CSS properties are changed and then a browser reflow event is triggered and the transition starts. In other words, the transition property is set to none, then the left property is changed, then the transition property is changed back to left 1s.

After all the style properties have been updated, a browser reflow event is triggered, the CSS is repainted, and then the transition starts:

element.style.transition = "none";
element.style.left = px + "px";
element.style.transition = "left 1s";
// The transition starts after all the CSS has been modified and a reflow has been trigged.

There are a few reasons it is executed like this.

The main reason is performance. Rather than repainting each element after changing a single CSS property, it is much more efficient to change all of the properties and have a single repaint event. Additionally, if you are transitioning multiple properties you would expect each property to be changed before the element is transitioned (which is exactly what is happening).

If you want a clean alternative that doesn't involve any timeouts/delays or forced reflows, you could simply set the transitionDuration property to 0s before setting the value, and then remove this inline style when transitioning the element.

For example:

var element = document.getElementById("theElement");

function setX(px) {
  element.style.transitionDuration = '0s';
  element.style.left = px + "px";
}

function animateX(px) {
  element.style.transitionDuration = '';
  element.style.left = px + "px";
}
#theElement {
  position: relative;
  width: 100px;
  height: 100px;
  border: 2px solid;
  left: 200px;
  transition: left 1s;
}
<div id="theElement"></div>

<button onclick="animateX(100)">Transition 100</button>
<button onclick="animateX(300)">Transition 300</button>
<button onclick="setX(0)">Set 0</button>
<button onclick="setX(50)">Set 50</button>

Similarly, you could also toggle a class on the element before transitioning or setting the value:

var element = document.getElementById("theElement");

function setX(px) {
  element.classList.remove('transition');
  element.style.left = px + "px";
}

function animateX(px) {
  element.classList.add('transition');
  element.style.left = px + "px";
}
#theElement {
  position: relative;
  width: 100px;
  height: 100px;
  border: 2px solid;
  left: 200px;
}
#theElement.transition {
  transition: left 1s;
}
<div id="theElement"></div>

<button onclick="animateX(100)">Transition 100</button>
<button onclick="animateX(300)">Transition 300</button>
<button onclick="setX(0)">Set 0</button>
<button onclick="setX(50)">Set 50</button>

The above snippet works, but it's also worth pointing out that you can listen to the transitionend event and remove the class when the transition ends:

For instance:

function animateX(px) {
  element.classList.add('transition');
  element.style.left = px + "px";
  element.addEventListener('transitionend', callback);

  function callback() {
    this.classList.remove('transition');
    this.removeEventListener('transitionend', callback);
  }
}
Community
  • 1
  • 1
Josh Crozier
  • 233,099
  • 56
  • 391
  • 304
  • Great answer. I didn't know you could listen to the `transitionend` event. Does this event have good browser support? – jperezov Jan 11 '16 at 20:26
  • 1
    @jperezov Yes, it has [good browser support](https://developer.mozilla.org/en-US/docs/Web/Events/transitionend#Browser_compatibility). It's supported in all modern browsers, although you may need to use prefixed version for older browsers, for instance, `webkitTransitionEnd`, `msTransitionEnd`. – Josh Crozier Jan 11 '16 at 20:28
3

You can do this but you have to restore the old transition property with a timeout.

function setX(px) {
    element.style.transition = "none";
    element.style.left = px + "px";
    setTimeout(function() {
        element.style.transition = "left 1s";
    });
}

JSFiddle

If you don't use a timeout to restore the old transition property, the browser will never know it has changed at all. It won't render the page unless your code has executed completely.

I also recommend to unset the transition property like this:

element.style.transition = "";

Otherwise you override the stylesheet forever for this element. Before you set the transition style on the element, the transition is determined by the stylesheet. If you set the transition style on the element, you override the stylesheet.

Update: It seems the timeout is not sufficient. I tried it in FF/Linux and it did not work. So you either add some more milliseconds (not recommended) or you apply the solution of @jperezov (recommended).

lex82
  • 11,173
  • 2
  • 44
  • 69
  • Also, doesnt the setTimeout need a second argument for milliseconds? –  Jan 11 '16 at 16:19
  • It doesn't. Default is zero, so there is practically no delay. – lex82 Jan 11 '16 at 16:20
  • 1
    What do you mean by overriding the stylesheet forever, can you please give an example? –  Jan 11 '16 at 16:21
  • It is not a good idea to recommend setTimeout without knowing what's going on behind the curtain, which is better explained in Josh Crozier's answer. – Jan Turoň Oct 28 '20 at 08:45
0

Just to make some additions to fully sufficient and well explained Josh Crozier's answer:

There also exists ontransitioncalcel event which fires when the event is cancelled, so you can test if some methods cancel it. From the current draft I cherrypick:

If an element is no longer in the document, implementations must cancel any running transitions

so you can remove the element, do the changes and then add the element back to it's original position; setting display: none has various side effects, stopping transition among them,

If the element has a running transition for the property, and there is not a matching transition-property value, then implementations must cancel the running transition

so you can attempt to remove the transition-property value,

If the current value of the property in the running transition is equal to the value of the property in the after-change style, or if these two values are not transitionable, then implementations must cancel the running transition.

so you can set the left property to auto or initial first to break the transition.

Alas, none of transition events is cancellable, so calling e.preventDefault() in them shouldn't stop them.

Jan Turoň
  • 31,451
  • 23
  • 125
  • 169