This content originally appeared on Bram.us and was authored by Bramus!
A shortcoming of MutationObserver
(imho) is that it cannot be used to subscribe to value changes of CSS properties.
To plug that hole I built a library allowing just that: @bramus/style-observer
. It allows you to attach JavaScript callbacks to changes in computed values of CSS properties.
It differs from previous attempts at doing this by not relying on requestAnimationFrame
. Instead it is powered by CSS Transitions and transition-behavior: allow-discrete
.
~
Demo
Let’s jump straight in with a demo. Every time you click the document, the background-color
is set to a random color. In response to each change, thanks to @bramus/style-observer
, a callback gets executed. This callback shows the new computed value in a notification using the notyf
library.
See the Pen @bramus/style-observer demo by Bramus (@bramus) on CodePen.
~
Installation and Usage
To obtain @bramus/style-observer
install it through NPM (or whatever package manager you are using):
npm install @bramus/style-observer
The following code shows you how to use it. See the inline comments for explanation on what each section does:
// Import the CSSStyleObserver class
import CSSStyleObserver from "@bramus/style-observer";
// Array with the names of the properties to observe.
// This can be one or more, both regular properties and custom properties
const properties = ['background-color'];
// Create a CSSStyleObserver that tracks the properties.
// Every time one of those properties their computed value changes, the passed in callback gets executed
// Here, the callback shows the new computed value in a notification
const cssStyleObserver = new CSSStyleObserver(
properties,
(values) => {
showNotification(values['background-color']);
}
);
// Have the CSSStyleObserver instance observe the `<body>` element
cssStyleObserver.attach(document.body);
// Change the background-color every time you click the document
document.documentElement.addEventListener('click', (e) => {
document.body.style.setProperty('background-color', randomBackgroundColor());
});
~
Under the hood
Under the hood @bramus/style-observer
relies on CSS Transitions and transition-behavior: allow-discrete;
.
Each property that you monitor with @bramus/style-observer
gets a short CSS Transition applied to it. The transition is set to a very short transition-duration
of 0.001ms
and the transition-timing-function
is set to step-start
so that the transition immediately kicks in.
To catch this transition, the library also sets up a transitionstart
event listener which invokes the callback that was passed into the CSSStyleObserver
.
By default, transitions only fire for properties that don’t animate discretely. Thanks to the very recent transition-behavior: allow-discrete;
, it is now possible to have transitions – along with their events – on properties that animate discretely (which includes Custom Properties).
~
Browser Support
Technically speaking, @bramus/style-observer
works in any browser that supports CSS Transitions. To also observe properties that animate discretely, support for transition-behavior: allow-discrete;
is also required so in practice that boils down the the following browsers that are supported:
- Chrome/Edge 117 (*)
- Firefox 129
- Safari 18
However, Chrome is currently affected by https://crbug.com/360159391 in which it does not trigger transition events for unregistered custom properties. You can work around this Chrome bug by registering the custom property using @property
.
But … Safari doesn’t like that workaround as it then seems to be stuck in a transition loop when the custom property is registered with a "<string>"
or "*"
syntax. Other syntaxes – such as "<number>"
or "<custom-ident>"
(which kinda look like a string) don’t mess up things in Safari (and also bypass that Chrome bug).
~
Prior Art and Acknowledgements
This section is purely informational.
The requestAnimationFrame
days
Wanting a Style Observer is not a new idea. There have been attempts at making this before such as ComputedStyleObserver
by keithclark (2018) and StyleObserver
by PixelsCommander (2019).
Both rely on using requestAnimationFrame
, which is not feasible. This because requestAnimationFrame
callbacks get executed at every frame and put a load on the Main Thread.
Furthermore, the callback used in those libraries would also typically trigger a getComputedStyle
and then loop over all properties to see which values had changed, which is a slow process.
Besides putting this extra load on main thread, looping over the getComputedStyle
results would not include Custom Properties in Chrome due to https://crbug.com/41451306.
And finally, having a requestAnimationFrame
forces all animations that run on the Compositor to also re-run on the Main Thread. This because getComputedStyle
needs to be able to get the up-to-date value.
Add all those things up, and it becomes clear that requestAnimationFrame
is not a feasible solution 🙁
The CSS transitions approach
In 2020, Artem Godin created css-variable-observer
which ditched the requestAnimationFrame
approach in favor of the CSS Transitions approach. While that library is more performant than the previous attempts it has the big limitation that it only works with (custom) properties that contain <number>
values.
This is due to the (clever!) approach to storing all the data into the font-variation-settings
property.
The choice for font-variation-settings
was made because its syntax is [ <opentype-tag> <number> ]#
(with <opentype-tag>
being equal to <string>
) and it’s a property that is animatable.
🙏 The code for @bramus/style-observer
started out as a fork of css-variable-observer
. Thanks for your prior work on this, Artem!
Transitioning discretely animatable properties
Just two days ago, former colleague Jake Archibald shared a StyleObserver experiment of his in the CSS Working Group Issue discussing this. His approach relies on Style Queries and a ResizeObserver
to make things work.
In the follow-up discussion, Jake foolishly wrote this:
Huh, could some of the discrete value animation stuff be used to make this work for non-numbers?
Whoa, that was exactly the clue that I needed to go out and experiment, resulting in this Proof of Concept. That POC was then used to build @bramus/style-observer
into what it is now 🙂
🙏 Thanks for providing me with the missing piece of the puzzle there, Jake!
~
Spread the word
Feel free to repost one of the posts from social media to give them more reach, or link to this post from your own blog.
Introducing @bramus/style-observer, a MutationObserver for CSS.
It allows you to attach JavaScript callbacks to changes in computed values of CSS properties.
🔗 https://t.co/6XlDe7Ixd0 pic.twitter.com/3lL2rnfiRg
— Bram.us (by @bramus) (@bramusblog) August 30, 2024
~
🔥 Like what you see? Want to stay in the loop? Here's how:
This content originally appeared on Bram.us and was authored by Bramus!
Bramus! | Sciencx (2024-08-30T23:15:29+00:00) Introducing @bramus/style-observer, a MutationObserver for CSS. Retrieved from https://www.scien.cx/2024/08/30/introducing-bramus-style-observer-a-mutationobserver-for-css/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.