Let's start with the basics. What is CSSOM?
The OM in CSSOM stands for Object Model. It's how JS interfaces with CSS in the DOM.
You're probably familiar with CSSOM, but maybe not the name. It's this:
const div = document.querySelector('div');
div.style.marginLeft = '20px';
div.style["padding-right"] = '5px';
div.style.transform = 'translate(20px, 10px)';
See? The CSS is represented in an Object Model which lets us edit values and see them reflected in real time. We can set styles using good ole JS camel case (marginLeft
), or CSS kebab case (margin-left
).
Messy CSSOM
The CSSOM is fine and dandy in most instances, but anyone who has worked with it for long enough knows that there are situations where the worse side rears its ugly head. Let's create some of those situations.
const div = document.querySelector('div');
div.style.position = 'relative';
div.style.left = '10' + (Math.random() > .5 ? 'px' : 'rem');
Now we have moved our div either 10px or 10rem to the right. Let's say we want to move it 10 additional units to the right, regardless of the unit type.
const num = parseFloat(div.style.left) + 10;
const unit = div.style.left.slice(String(num).length);
div.style.left = num + unit;
At first I'm using a parseFloat
trick to just get the number, but when we want to append the unit, it gets a bit trickier. Since rem
, unlike most other unit values, has a length of three, we're using a different method which can handle any unit length.
It works, but it's definitely not clean or very predictable. Let's show another case.
const div = document.querySelector('div');
const randLightness = Math.floor(Math.random() * 50) + 30; // between 30 and 79
div.style.background = `hsl(100, 100%, ${randLightness}%)`;
Now let's retrieve the color and get its lightness value.
const lightness = div.style.background.split(',')[2].slice(1, -1);
console.log('lightness: ' + lightness); // > "lightness: 0"
That's weird. We put in a lightness of 30% - 79% and got back "0". There's no percentage either. I know my JS is right. I think so, at least. Let's console log the color and see what we're getting.
console.log(div.style.background) // > "rgb(68, 204, 0)"
What? We put in an hsl()
format. We seem to be getting back an rgb()
representation of hsl(100, 100%, 40%)
. If we want to increase the lightness by ten, we're going to have to try a much different method than last time.
CSSOM's Hazardous Consistencies
I touched on this in a previous article of mine surrounding a FireFox problem with transform: translate()
.
The CSSOM has a spec which describes how it handles values passed into it. It will convert any color format into rgb()
when it can. It's specified as follows:
If is a component of a resolved or computed value, then return the color using the rgb() or rgba() functional notation as follows
The "as follows" part isn't very interesting aside from how it specifies that if you pass rgb()
an alpha parameter, it will convert it to rgba()
. What's important is how it will always return a color in rgb()
or rgba()
format.
This is horrible if we want to edit a property using the hsl()
format. We'd have to use complex math to convert it to hsl()
before editing and sending it back.
The one way to prevent a lot of hassle is to use a CSS variable in place of one of the parameters. This prevents the engine from parsing it, since that would lead to much different results.
div.style.background = 'hsl(100, 100%, calc(var(--lightness) * 1%))';
const randLightness = Math.floor(Math.random() * 50) + 30; // between 30 and 79
div.style.setProperty('--lightness', randLightness);
NOTE: You must use .setProperty
to edit CSS variables. Setting it on the style property itself using style['--lightness']
will not work. You can verify this by logging div.style.cssText
when setting either.
But using CSS variables seems more like a patch than a solution. The CSS itself looks a bit messy and if a third party wanted to edit our CSS they'd have to recognize the existence of the CSS variable in order to make any changes.
The CSSOM makes a lot of changes like these. You can play around and see what gets changed by going to this site by Tom Hodgins. It's a fantastic little tool that lets you see how the engine parses your CSS in real time. It's primarily used to see if your browser supports a specific feature, but this is an excellent use case as well.
Nov 21, 2019 edit: About 5 minutes ago, we have come under the realization that he is CSSTOM Hodgins. Please address him as such in future encounters.
This CSSOM behavior gets even messier when changing values of css properties that accept multiple parameters.
CSSOM's Hazardous Cleanup
According to #2.2 in section 6.7.2 - Serializing CSS Values in the spec:
If component values can be omitted or replaced with a shorter representation without changing the meaning of the value, omit/replace them.
If possible, CSS values are cleaned up by replacing and omitting parts to make them shorter. This seems like a good idea in theory, but when passing values that can be shortened, they almost always will.
Using the same tool mentioned in last section, we can verify that margin: 1px 1px;
will be shortened to margin: 1px;
in every browser, but transform: translate(20px, 0px);
will only be shortened to transform: translate(20px);
in firefox.
Each of these shortenings have the exact same functionality in all browsers, but sometimes only one browser will choose to shorten it.
Due to the spec, this issue with FireFox was declared not to be a bug in response to a report on Bugzilla.
As stated [on the spec], we may change this for compatibility with other engines, but I think lacking broken sites, the Gecko behavior is preferable / more consistent / more correct.
This all means that when we grab values from CSSOM, we have to be wary that it might not contain all the parameters we originally passed into it. This leads to a lot more logic and messy-looking code each time.
Luckily, the Houdini initiative aims to solve all of this. It currently does not address all of our problems, but hopes to soon do so.
Introducing CSS Typed OM
Like how JS has the types 'String', 'Number', 'Boolean', etc, CSSOM is getting its mini type system as well. They are accessible through the global CSS
variable and are used as follows:
const div = document.querySelector('div');
div.attributeStyleMap.set('margin-left', CSS.px(23));
const margin = div.attributeStyleMap.get('margin-left')
console.log('margin-left: ' + margin); // > "margin-left: 23px"
Now this is certainly more verbose and CSS Typed OM will often be more verbose, but the typed system is a lot more safe, as types usually are.
The new way to access styles is through a Map
-like object called attributeStyleMap
instead of just style
. Like Maps, it has all the usual methods: get, set, has, etc. It's also an iterator, so it's loopable in a for ...of
.
When retrieving the style, a bit of formatting goes on. You get an object containing a value
and unit
property. When calling the .toString()
method, you get a concatenated string instead of an object. Concatenating the unit object will call the .toString()
method, which is why the example above didn't include it.
How does this solve our problems? Let's start with one of the basic principles:
What you put in is what you get back. It does a lot of formatting for us so that our CSS is valid, but we can still retrieve what we put in.
Let's set up an example:
const div = document.querySelector('div');
div.attributeStyleMap.set('z-index', CSS.number(4.45143));
What we have here is a z-index with a really peculiar z-index. Let's say that we're animating our z-index in a very specific fashion, so these decimal places are important to us.
CSSTOM will format this for us and truncate off the decimals, but when we request it later, our decimals will be preserved
div.attributeStyleMap.get('z-index').values[0].value; // -> 4.45143
div.computedStyleMap().get('z-index').value; // -> 4
Note 1: We can get the parsed value using .computedStyleMap()
instead of .attributeStyleMap
.
Note 2: I used .values[0].value
instead of .value
since at the time of writing this article, there seems to be a bug where decimal values in CSSTOM z-index will generate a CSSMathSum object.
This behavior will in the future (I got confirmation from Tab Atkins Jr.) also extend to colors. The value will still be parsed into rgb()
format, but we will be able to retrieve our hsl()
representation.
Transform functions are also much more organized. We are additionally going to be getting a separate translate
and scale
css property for even better access in the future.
CSSTOM Now
At the time of writing this article, CSS Typed OM is only available in part on Chrome (Chromium, so Opera too and soon Edge) exclusively. The spec itself is still being written, so it may be some time before we see a full implementation.
In my opinion, CSSTOM is the only Houdini initiative that really fixes something. The other features coming along are absolutely fantastic as well, but more about additions than fixes. CSSTOM cannot come any sooner.
Top comments (0)