CSS Math Eval, now Better and Weirder
- Published on:
- Categories:
- Observation 9, Custom Elements 4, CSS 65
- Current music:
- Uchu Conbini — 体温
- Current drink:
- Peppermint Tea
A Better Solution
After the yesterday’s “Observation: CSS Math Eval” post, I got a mastodon reply from Valtteri Laitinen (in a reply thread started by Kilian Valkhof):
Seems that you can use
CSSNumericValue .parse(`calc(${el.value})`) .to('number').value
instead of the custom property and
getComputedStyle()
. That isn’t supported by Firefox though.
When I was experimenting for my original post, I briefly looked at the CSSOM, but did miss the .to('number')
method! I suspected that it should provide something similar, and I am happy that it exists, and that Valtteri pointed it out to me!
For browsers that support CSSNumericValue
, this is a much better solution, as it does not require getComputedStyle()
, and thus should not cause a layout recalculation.
Here is the example from the last article, now using CSSNumericValue
when it is available:
<css-math-2>
<input type="text" value="2 + 2" />
=<output></output>
</css-math-2>
<css-math-2>
<input type="text" value="sin(90deg)" />
=<output></output>
</css-math-2>
<css-math-2>
<input type="text" value="(pi - 1) * 2" />
=<output></output>
</css-math-2>
<script>
CSS.registerProperty({
name: '--css-math-2',
syntax: '<number>',
inherits: false,
initialValue: 0,
});
class CSSMath2 extends HTMLElement {
getResult(el) {
if ('CSSNumericValue' in window) {
return CSSNumericValue
.parse(`calc(${el.value})`)
.to('number').value
+
' (using CSSNumericValue)';
}
this.style.setProperty(
'--css-math-2',
`calc(${el.value})`
);
return getComputedStyle(this)
.getPropertyValue('--css-math-2')
+
' (using getComputedStyle)';
}
connectedCallback() {
const input = this.querySelector('input');
const output = this.querySelector('output');
setTimeout(() => {
output.value = this.getResult(input)
}, 0);
input.addEventListener('input', ({ target }) => {
output.value = this.getResult(target);
});
}
}
customElements.define('css-math-2', CSSMath2);
</script>
A Better Fallback
However, if we’re talking about fallbacks, my original method was still far from being perfect: after all, registerProperty
has a worse support.
Can we have a better fallback for it? Yes, if instead of using a registered custom property, we will use a regular property that accepts a <number>
syntax:
<css-math-3>
<input type="text" value="2 + 2" />
=<output></output>
</css-math-3>
<css-math-3>
<input type="text" value="sin(90deg)" />
=<output></output>
</css-math-3>
<css-math-3>
<input type="text" value="(pi - 1) * 2" />
=<output></output>
</css-math-3>
<script>
let propertyToCheck = 'border-image-outset';
if ('registerProperty' in CSS) {
propertyToCheck = '--css-math-3';
CSS.registerProperty({
name: propertyToCheck,
syntax: '<number>',
inherits: false,
initialValue: 0,
});
}
class CSSMath3 extends HTMLElement {
getResult(el) {
if ('CSSNumericValue' in window) {
return CSSNumericValue
.parse(`calc(${el.value})`)
.to('number').value
+
' (using CSSNumericValue)';
}
this.style.setProperty(
propertyToCheck,
`calc(${el.value})`
);
return getComputedStyle(this)
.getPropertyValue(propertyToCheck)
+
` (using ${propertyToCheck})`;
}
connectedCallback() {
const input = this.querySelector('input');
const output = this.querySelector('output');
setTimeout(() => {
output.value = this.getResult(input)
}, 0);
input.addEventListener('input', ({ target }) => {
output.value = this.getResult(target);
});
}
}
customElements.define('css-math-3', CSSMath3);
</script>
Just for fun (and because it is unlikely to clash with anything usually), I used the border-image-outset
property. Did you know about its existence? It is there for as long as calc()
, which we also need for things to work.
A Weird Solution: Calculated initial-value
Before I got to know about CSSNumericValue
, I was playing with various ways to improve the performance of my initial method. I found a thing about which I did not know about, but which seems obvious in hindsight.
We can use calculations for initial-value
of registered properties! Here is my original example which uses this method:
<css-math-weird>
<input type="text" value="2 + 2" />
=<output></output>
</css-math-weird>
<css-math-weird>
<input type="text" value="sin(90deg)" />
=<output></output>
</css-math-weird>
<css-math-weird>
<input type="text" value="(pi - 1) * 2" />
=<output></output>
</css-math-weird>
<script>
const mathCache = new Map();
class CSSMathWeird extends HTMLElement {
getResult(el) {
const result = mathCache.get(el.value);
if (result) {
return result;
}
const name =
`--css-math-weird-${mathCache.size}`;
CSS.registerProperty({
name,
syntax: '<number>',
inherits: false,
initialValue: `calc(${el.value})`,
});
const value = getComputedStyle(this)
.getPropertyValue(name);
mathCache.set(el.value, value);
return value;
}
connectedCallback() {
const input = this.querySelector('input');
const output = this.querySelector('output');
setTimeout(() => {
output.value = this.getResult(input)
}, 0);
input.addEventListener('input', ({ target }) => {
output.value = this.getResult(target);
});
}
}
customElements.define(
'css-math-weird',
CSSMathWeird
);
</script>
Ok, this one is very inefficient: we need to maintain a cache for the values (so we won’t attempt to register the same custom property twice, which is prohibited), and I don’t even want to know how the browser will behave if we will suddenly register hundreds of unused custom properties.
But hey, it works! Not sure what can be the practical purpose of this, though.
Room for Improvement
There are still many things that something like this will require for it to be a viable reusable solution.
But now I know about CSSNumericValue
! I hope we will get it in all browsers, eventually. Mozilla is positive about CSS Typed OM, so, hopefully, someone will eventually implement it there.
It is fascinating how by implementing a powerful feature like this one in CSS, we unlock it not only there, but also for JS as well. Can it be viable to propose a native JS method for evaluating CSS math?
I’m also happy that I wrote the original post, even though the idea was very wild and unpolished. It might help someone (like Kilian), and now we know more about CSS and its API.