The Hazards of Passing CSS Through JS - A Look Into CSS Transform and Inconsistencies
Published: 2019-07-08 19:48:19

I love CSS. It's my baby. I will cherish CSS forever, but let's be honest. It's a bit of a mess.

Through the years, backwards compatibility and browser support has made CSS (and JS, but that's not what this article is about) a bit messy. While different browser engines won't always affect the outcome of some JS, only the speed, the same can't be said for CSS.

Different engines have different ways of handling styles. It's rather subtle, but it ends up leading to *really* annoying bugs later on in the development process. One specifically fickle CSS property is the CSS `transform`.

It can be used to move and manipulate elements. [Yair Even Or](https://codepen.io/vsync/) made this neat codepen to see just what transforms are capable of: https://codepen.io/vsync/pen/RayMgz

It's a bit odd since, similar to the [filter](https://codepen.io/EmNudge/pen/JxdpYq) property, it doesn't really do just *one* thing. It can be used for a multitude of things. It transforms them. How? That's up to you.

```css
transform: translate(10px, 20px); /* moves to the x,y coords of (10, 20) */
transform: rotate(15deg); /* rotates 15 degrees clockwise */
```

You may often find yourself needing more than one transform at a time. Listing them like this will overwrite the previous one. We're instead able to combine them.

```css
/* moves to the x,y coords of (10, 20) and rotates 15 degrees clockwise */
transform: translate(10px, 20px) rotate(15deg); 
```

Got it? Cool. It's only uphill from here... Right?

# The Good

Transforms are real nice since they don't require any layout-based calculations. [Paul Irish](https://www.paulirish.com/2012/why-moving-elements-with-translate-is-better-than-posabs-topleft/) makes a nice case for using `transform: translate(10px, 10px)` over absolute positioning using `left: 10px` and `right: 10px` with `position: absolute;`. 

Transforms are not only better for performance, they allow for sub-pixel positioning. You can move an element to the position of (20.3423, 4.5294) without any errors. Why would you want this? Cursors move using sub-pixels. If you want any drag interaction, using absolute positioning might do what's referred to as "dubstepping." I highly recommend checking out [Paul Irish's article](https://www.paulirish.com/2012/why-moving-elements-with-translate-is-better-than-posabs-topleft/) for more information on that.

Now, despite the upsides, the complex nature of `transform` leads to a lot of annoyances. One such annoyance is regarding the dreaded blur.

# Blur

Since transforms are calculated differently behind the scenes, the way the elements are displayed at the end of the day can also be affected. During transform animations, the element may experience a slight amount of blur. Since we're dealing with sub-pixels, this is understandable. It's annoying, but understandable.

Unfortunately, this behavior is *super* inconsistent. Not only between browsers, but within the same browser in almost identical environments. Chrome seems to be the biggest culprit. It's difficult to reproduce blur, but it will usually show up during transform animations. I first noticed this a while back when working on an idea I had: a website for microphone comparisons.

You can see the early prototype [here](https://emnudge.bitbucket.io/), but notice how the microphone boxes grow a bit on hover. Transform is just about the only way to achieve such an effect using `transform: scale()`, as simple as it is, but I ran into a problem. On some browsers, on some operating systems, sometimes the boxes would blur during or even after the animation. 

I ended up with a clever solution where the boxes are initially scaled down and only resize back to normal on hover. This would ensure the box wasn't resting on any sub-pixels when hovered over. If it was smaller and blurry it didn't matter as much - only if it blurred when enlarged.

Although, this only fixed it *most* of the time. I soon forgot all about this and was only reminded of it when working on a project involving [draggable windows](https://codesandbox.io/s/window-react-component-2zl5z).

Here is where it gets problematic. We're manipulating transforms through JS.

# Manipulating CSS With JS

Let's say we'd like to change the padding of an element to 5px more than what it originally was. Assuming this padding is on an element with the class of `box`, we'd probably do something like this:

```js
const box = document.querySelector('.box');
const padding = parseInt(box.style.padding, 10);
box.style.padding = padding + 5 + 'px';
```

We're getting the box, getting its padding px value as a number, and then giving it back with 5 more, tacking on the 'px' so that it knows which unit type we're using. A bit much, but bearable. 

As a side note, we're using a trick with `parseInt()`. Using `Number()` would return a `NaN` since it includes the letters `px`. `parseInt()` will get us the number *until* it reaches a string. It will only ever return a `NaN` if the first character isn't parsable. feeding it a `px` string will give us the number until the `px`.

Now let's try adding 10px to the top and bottom and 20px to the sides. Why? Just for the hell of it.

```js
const box = document.querySelector('.box');
const [paddingY, paddingX] = box.style.padding.split(' ').map(val => parseInt(val, 10));
box.style.padding = `${paddingY + 10}px ${paddingX + 20}px`;
```

Woah. We were able to use the same amount of lines, but now it's a bit more complicated. We're assuming the CSS *already* had a padding of 2 values, separated by a space, but it might not have. We could have passed parseInt without a radix, shortening the map to just `.map(parseInt)`, but it's bad practice. We also used array destructuring to save some more space.

Now let's move on to CSS transforms. Let's handle a very real case of moving a box 10.513 more pixels to the right and 3.422 more pixels upward. We'd get such a situation with draggable boxes using CSS transforms.

```js
const box = document.querySelector('.box');
const matchTranslate = new RegExp(/translate\(.+?\)/);
const translateStr = box.style.transform.match(matchTranslate)[0];
const translatePos = translateStr.slice(10, -1).split(',');
const [x, y] = translatePos.map(val => parseFloat(val, 10));
const newTransStr = `translate(${x + 10.513}px, ${y - 3.422}px)`
box.style.transform = translateStr.replace(matchTranslate, newTransStr);
```

Ouch. Regex? Yup. We could do this with a few higher order functions, but regex is going to be faster and more clear (Regex? Clear? What?).

Since the transform property can include multiple transforms we have to find and return our transform without touching the others. Our values are also contained within `transform(${rightHere})` so we have to dig a bit to get at them before we can even parse them into numbers using `parseFloat` (it's the same as parseInt, but for decimals now. Transforms deal with sub-pixels, remember?).

Now here comes some more frustration, which is one of the reasons I sat down to write this article. There are some things that different browsers do with transforms that aren't outlines very well in any documentation.

# Optional Values and CSS Parsing

You see, translate's second value is optional. Only adding one px value is valid. It will simply just move in the x direction. Note that this is different than `scale()`, which also has a second option value, but acts differently.

If `scale()`'s second value is left out, it will scale proportionally in both directions. It's the same deal with properties like the aforementioned `padding`. Leave one value out and it'll pad in all 4 directions. `translate()`, however, will move exclusively on the x axis. 

It's a bit weird, but nothing too odd, right? Not exactly.
We discussed weird browser behavior before. Here is another one to add onto the pile. 

When manipulating CSS values in JS, the browser might clean up the values a bit before using them. If you set the `transform` of your box to `translate(20px,       1px)`, when retrieving your value, you will find all those spaces condensed. It will return as a much cleaner `translate(20px, 1px)`. 

Again, [odd-ish](https://bulbapedia.bulbagarden.net/wiki/Oddish_(Pok%C3%A9mon)), but nothing to worry about just yet. Here's where it gets interesting.

Since only one value passed to `translate()` will move it in the X direction, what happens if we pass it `translate(20px, 0px)`? In this case, it's identical to `translate(20px)`. Will the browser clean it up? Well, it depends. On Chrome, Edge, and Safari it won't, but on Firefox it will. What happens if we run our previous code when out draggable box is on the position (20, 0)? There shouldn't be any problem... right?

Unfortunately, our code gets condensed. Our `y` value will be undefined and any math we do will result in a `NaN`. This means our `y` position will from now on be stuck whenever it hits 0, but only in FireFox.

Since this information isn't exactly listed anywhere at surface level, finding this was a pain. How can we prevent something like this from creeping up on us in the future?

# Solution 01 - Local State

The most obvious one is probably storing all our changing css values as state. If we keep a local state of all our current css values, we can automatically generate a new css string when we need one.
```js
let skew = 0,
    translateX = 0,
    translateY = 0,
    rotate = 0;

function getTransform(skew, translateX, translateY, rotate) {
    return `skew(${skew}deg) translate(${translateX}px, ${translateY}px) rotate(${rotate}deg)`
}
```
This makes it much easier to change the transform. We don't need to care about the positions of anything or what the browser returns. We're not getting anything, only every setting.

Now it happens to be that I was forbidden from keeping any local state in my specific example. The specific are irrelevant, but if such a case comes up, we're not gonna have a very fun time.

Let's try another method

# Solution 02 - CSS Variables
CSS variables have support across all major browsers (excluding IE of course). The major benefit of native CSS variables over pre-compiled/transpiled variables like ones from Sass or Less, is that CSS variables can be accessed and altered through JS.

We can define CSS variables in our `transform` example as follows.
Credit goes to [Tommy Hodgins](https://twitter.com/innovati) for this idea.
```css
.box {
  /* ...other styles */
  --pos-x: 0;
  --pos-y: 0;
  transform: translate(
    calc(var(--pos-x) * 1px),
    calc(var(--pos-y) * 1px)
  );
}
```
Using this syntax, we have set up 2 CSS variables named `--pos-x` and `--pos-y`. These variables are passed through the `translate()` function and turned into pixel values by using `calc()`. By multiplying the values by 1px, we are effectively converting them into pixels from whatever they were beforehand. Look Ma! No JS!

Whenever we want to manipulate the position, we just write some JS such as
```js
const box = document.querySelector('.box');
const pos = { x: 20, y: 75 };
box.style.setProperty("--pos-x", pos.x);
box.style.setProperty("--pos-y", pos.y);
```
and voila! Our box will have moved!
We didn't have to extract the values in any weird way, write tons of lines of code, or deal with any unit conversion in the JS. The CSS does most of the work for us!

This would probably be the optimal solution for most circumstances. The problem comes when you want to start manipulating many values and switching over to the CSS to create new variables becomes a bit cumbersome. While not completely available yet, the Houdini effort is on its way to fully introduce the **CSS Typed OM**.

# Solution 03 - CSS Typed Object Model
[Eric Bidelman](https://developers.google.com/web/updates/2018/03/cssom) gave a nice write up on what it is. I highly recommend reading up over on [google developers](https://developers.google.com/web/updates/2018/03/cssom) about it.

Essentially, it's an Object API for CSS. It lets us interact with the CSS in a much cleaner way. Instead of getting strings, we're getting nested objects that are organized according to what we're getting and setting.
Here's an example straight from Eric's post (with some edits):

```js
const box = document.querySelector('.box');
box.attributeStyleMap.set('padding', CSS.px(42));
const padding = el.attributeStyleMap.get('padding');
console.log(padding.value, padding.unit); // 42, 'px'
```

Here we are setting and then getting the padding. It's a bit more verbose, but it is now *much* clearer what we're doing. I wanted to show an example using `transform`, but the support for CSSOM seems to be too weak right now. CSSOM is a part of the [Houdini](https://developers.google.com/web/updates/2016/05/houdini) initiative and only has support in Chrome from version 66. The MDN docs are fairly sparse and its use with properties such as `transform` and `filter` which can contain multiple styling options is fairly poor. For now.

The Houdini effort in general is fantastic and its introduction is much needed. 

Well, while we're at a bit of an impasse, there is yet another option