Tweening in CSS

Arnošt Neurad
4 min readApr 23, 2019

From an urgent need to hack an existing CSS transition to a tweenable animation rose this experiment, where I've turned a transition into a thing controllable via JS.

The demo can be seen here: https://codepen.io/paralleluniv3rse/pen/WWdRdQ

The whole transition is defined purely in CSS, which means you can control and change the property values and easings solely inside your stylesheets using classes, HTML attributes, and all that CSS provides! Ain't that great?

Let me explain what this is and how it works.

The basics are simple, we have a red box which we can transform to the right by adding the .right class.

The grey background is another scrollable element and based on it's scroll position we are going to be progressing our red box tween. The tweening of course can be tied to any trigger, not just scrolling.

We also have a .tweening class for the box which is one of the key parts for CSS tweening and basically sets the transition time to be suuuuper long and easily divisible by 100, which makes thinking about the progress of our tween easier (we'll touch on that later).

Inside our Javascript things don't get much more complicated.

At the start of our tween we add the .tweening class to the .box element.
Now if the transition to the .right were to happen, it would take 100000 seconds. To our eye it would stay in place and do nothing.

While the scroll event is firing we get the scroll position of the scrollable element and turn it into a “percentage” number in the range between 0 and 1.

We apply a NEGATIVE transition delay of percentage * 100000 seconds to the .box and add the .right class to define where the tween ends.

An easy example — to display the transition at 57% progress, we set atransition-delay: -57000s .
Now the browser would display the transition as if 57000 seconds from our 100000 second transition have already elapsed and takes another 100000 seconds to finish the transition, effectively just keeping the box in place at the 57000 second mark.

This is where it gets a bit complicated, though. Now we need to force the browser to re-trigger the transition every time we change the negative delay, because just changing the transition-delay does not make the browser recompute the transition and display it again.

For that to happen, we need to change the transitioned property to some other value. That means every time we change the transition-delay we need to change the value of transform to some other value than the end value in .right , so the transition gets recalculated and then immediately change it back again to display the transition in the correct place. That's where these two handy functions come in:

const forceBoxToPlayTransition = (before = () => null, after = () => null) => {
before();
// –––––––– this sequence creates a new transition start spot based on the element's current location/progress ––––––––––––
box.style.transform = 'none';
forceStyleRecalculation();
box.style.transform = null;
// –––––––––––––––––––––––––––––––––––––––––––
after();
forceStyleRecalculation();
}
const forceStyleRecalculation = () => {
return document.body.offsetWidth;
}

There are two optional functions before() and after() which will come handy a bit later. For now, the important thing to note is — we change the transform to none flush all changes to CSSOM by getting document.body.offsetWidth then clear back the style by setting it to null, which allows back our style from .right to be used and flush style changes again.

Of course there is more issues.

Whenever we change the value of an ongoing transition, the new transition starts at the value our .box currently has. If we switch the transition back and forth at 60%, meaning with delay of -60000s, it would look something like this:

After changing to none box would move to position of .box1 on the image, because our delay tells it to be 60% of the way back to the none value from new starting position 1 . When we change transform back to value in our .right class, our delay tells it to be 60% of the way back to the .right from new starting position 2 . This moves the box to the position of .box2 on the image, instead of the correct place where we want it to be — back at the place before the change to none .

When we repeat this rapidly as on scroll in our case, this throws off our whole animation. Luckily there is a relatively easy fix.

forceBoxToPlayTransition(
() => {
box.style.transitionDuration = `0s`;
},
() => {
box.style.transitionDuration = null;
}
);

Using the before() and after() functions in the forceBoxToPlayTransition function, we can momentarily set the transition duration to 0s for the change to none and back. The change to noneis then executed immediately instead of being stuck at 60% on the way back, and we have no more misbehavior.

When done tweening — in our case when done scrolling — we simply remove the .tweening class from .box and clear the transition-delay property.

As a bonus I threw in an example of snapping to beginnig or end in the Codepen.

Enjoy and happy hacky tweening! ✌️

--

--