Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

introducing @bramus/style-observer, a MutationObserver for CSS #9

Open
innocces opened this issue Sep 7, 2024 · 3 comments
Open

introducing @bramus/style-observer, a MutationObserver for CSS #9

innocces opened this issue Sep 7, 2024 · 3 comments

Comments

@innocces
Copy link
Contributor

innocces commented Sep 7, 2024

The original blog info

subject content
title introducing @bramus/style-observer, a MutationObserver for CSS
url blog url
author bramus
@innocces
Copy link
Contributor Author

innocces commented Sep 7, 2024

Introducing @bramus/style-observer, a MutationObserver for CSS

August 31, 2024September 5, 2024 1 Comment on Introducing @bramus/style-observer, a MutationObserver for CSS

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 and by supporting properties that animate discretely (which includes Custom Properties). To achieve this, the library 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.

In CSS, transitions normally only fire for properties that can be interpolated. This does not include properties that animate discretely. Thanks to the very recent transition-behavior, it is now possible to have transitions – along with their events – on properties that animate discretely – which includes Custom Properties – after all. This is achieved by declaring transition-behavior: allow-discrete; onto the monitored element.

~

# 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

Note that all the browsers have bugs when transitioning Custom Properties. See the next section for details.

~

# A note on transitioning Custom Properties

There are a bunch of browser bugs – in all browsers – when it comes to transitioning custom properties.

Chrome for example 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.

Safari doesn’t like the Chrome workaround for certain syntaxes as it then seems to be stuck in a transition loop. This happens when the custom property is not registered or when the custom property is a string ("<string>", "*"/…). Other syntaxes – such as "<number>" and "<custom-ident>" – don’t mess up things in Safari (and also bypass that Chrome bug).

And Firefox finally doesn’t like it when a registered custom property uses a syntax with a type that can be interpolated.

UPDATE 2024.09.02 – I have gathered all issues on a dedicated site at https://allow-discrete-bugs.netlify.app/.

Right now, the only cross-browser way to observe Custom Properties with @bramus/style-observer is to register the property with a syntax of "<custom-ident>".

Note that <custom-ident> values can not start with a number, so you can’t use this type to store numeric values.

~

# 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

~

@innocces innocces changed the title [Recorder]: introducing @bramus/style-observer, a MutationObserver for CSS introducing @bramus/style-observer, a MutationObserver for CSS Sep 7, 2024
@innocces
Copy link
Contributor Author

innocces commented Sep 7, 2024

引入 @bramus/style-observer,一个 CSS 的 MutationObserver

2024年8月31日2024年9月5日 1条评论关于引入@ bramus/style-observer,CSS 的 MutationObserver

MutationObserver 的一个缺点是它不能用于订阅 CSS 属性的值更改。

为了填补这个漏洞,我构建了一个库,允许这样做:@bramus/style-observer。它允许您将 JavaScript 回调附加到 CSS 属性计算值的更改。

它与之前的尝试不同,它不依赖“requestAnimationFrame”,并且支持离散动画的属性(包括自定义属性)。为了实现这一目标,该库由 CSS Transitions 和“transition-behavior:allow-discrete”提供支持。

~

# 演示

让我们直接进入演示。每次单击文档时,“背景颜色”都会设置为随机颜色。为了响应每个更改,感谢“@bramus/style-observer”,执行回调。此回调使用 the notyf 在通知中显示新的计算值。

请参阅 [CodePen] 上 Bramus (@bramus) 的 Pen @bramus/style-observer 演示https://codepen.io)。

~

# 安装与使用

要获取“@bramus/style-observer”,请通过 NPM(或您正在使用的任何包管理器)安装它:

npm install @bramus/style-observer

以下代码向您展示了如何使用它。有关每个部分的作用的说明,请参阅内联注释:

// 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());
});

~

# 在引擎盖下

在幕后 @bramus/style-observer 依赖于 [CSS Transitions](https://developer.mozilla.org/en-US/docs /Web/CSS/CSS_transitions/Using_CSS_transitions)和transition-behavior:允许离散;

您使用“@bramus/style-observer”监视的每个属性都会应用一个简短的 CSS 转换。转换设置为“0.001ms”的非常短的“转换持续时间”,并且“转换计时功能”设置为“步进启动”,以便转换立即开始。

为了捕获此转换,该库还设置了一个“transitionstart”事件监听器,该监听器调用传递到“CSSStyleObserver”的回调。

在 CSS 中,过渡通常仅针对可插值的属性触发。这不包括离散动画的属性。感谢最近的 transition-behavior,现在可以进行转换 - 及其事件– 毕竟,在离散动画的属性上 – 其中包括自定义属性。这是通过在受监控元素上声明“transition-behavior:allow-discrete;”来实现的。

~

# 浏览器支持

从技术上讲,@bramus/style-observer` 适用于任何支持 CSS 过渡的浏览器。为了还观察离散动画的属性,还需要支持“transition-behavior:allow-discrete;”,因此在实践中归结为以下受支持的浏览器:

  • 铬/边缘 117
    *火狐浏览器129
  • 野生动物园 18

请注意,所有浏览器在转换自定义属性时都会出现错误。有关详细信息,请参阅下一节。

~

# 关于转换自定义属性的说明

在转换自定义属性时,所有浏览器中都存在大量浏览器错误。

例如,Chrome 目前受到 https://crbug.com/360159391 的影响,它不会触发未注册的自定义属性的转换事件。您可以通过使用 @property 注册自定义属性来解决此 Chrome 错误。

Safari 不喜欢 Chrome 对某些“语法”的解决方法,因为它似乎陷入了转换循环。当自定义属性未注册或自定义属性是字符串("<string>""*"/...)时,会发生这种情况。其他语法 – 例如 "<number>""<custom-ident>" – 不会在 Safari 中搞乱事情(并且也绕过 Chrome 错误)。

当注册的自定义属性使用具有可插值类型的“语法”时,Firefox 最终不喜欢它。

更新 2024.09.02 – 我已将所有问题收集到专用站点 https://allow-discrete-bugs.netlify.app/ 上。

目前,使用“@bramus/style-observer”观察自定义属性的唯一跨浏览器方法是使用“syntax”“”注册该属性。

请注意,“”值不能以数字开头,因此您不能使用此类型来存储数值。

~

# 现有技术和致谢

本节仅供参考。

requestAnimationFrame 时代

想要一个风格观察者并不是一个新想法。之前已经有人尝试过这样做,例如keithclark的CompulatedStyleObserver(2018)PixelsCommander的StyleObserver(2018)。 com/PixelsCommander/StyleObserver) (2019)

两者都依赖于使用“requestAnimationFrame”,这是不可行的。这是因为“requestAnimationFrame”回调会在每一帧执行,并在主线程上施加负载。

此外,这些库中使用的回调通常还会触发“getComputedStyle”,然后循环所有属性以查看哪些值已更改,这是一个缓慢的过程。

除了将额外的负载放在主线程上之外,循环“getComputedStyle”结果不会包含 Chrome 中的自定义属性,因为 [https://crbug.com/41451306]\(https://issues.chromium.org/issues/41451306 )。

最后,使用“requestAnimationFrame”会强制在合成器上运行的所有动画也在主线程上重新运行。这是因为 getComputedStyle 需要能够获取最新值。

把所有这些东西加起来,很明显 requestAnimationFrame 不是一个可行的解决方案🙁

CSS 过渡方法

2020 年,Artem Godin 创建了 css-variable-observer,它放弃了 requestAnimationFrame 方法,转而采用 CSS Transitions 方法。虽然该库比之前的尝试性能更高,但它有一个很大的限制,即它仅适用于包含“”值的(自定义)属性。

这是由于 (聪明!) 将所有数据存储到 font-variation-settings 属性中的方法。

选择“font-variation-settings”是因为它的语法是“[ ]#”(其中“”等于“”)并且它是可动画化的属性。

🙏 @bramus/style-observer 的代码最初是 [css-variable-observer](https://github .com/florumlabs/css-variable-observer/)。感谢您之前所做的工作,Artem!

转换离散动画属性

就在两天前,前同事Jake Archibald分享一个StyleObserver实验他在 CSS 工作组问题中讨论了这一点。 他的方法依赖于样式查询ResizeObserver让事情顺利进行。

在后续讨论中,Jake 愚蠢地 写道

嗯,可以使用一些离散值动画的东西来使这个适用于非数字吗?

哇,这正是我需要出去实验的线索,从而产生了这个概念证明。然后使用该 POC 将 @bramus/style-observer 构建为现在的样子 🙂

🙏 感谢您为我提供了拼图中缺失的一块,杰克!

~

[#](#spread-the word) 传播消息

请随意转发社交媒体上的一篇帖子,以扩大其影响范围,或者从您自己的博客链接到该帖子。

引入 @bramus/style-observer,一个 CSS 的 MutationObserver。

它允许您将 JavaScript 回调附加到 CSS 属性计算值的更改。

🔗 https://t.co/6XlDe7Ixd0 pic.twitter.com/3lL2rnfiRg

— Bram.us(@bramus)(@bramusblog)2024 年 8 月 30 日

~

@innocces
Copy link
Contributor Author

innocces commented Sep 7, 2024

core code

  this._targetElement.addEventListener('transitionstart', this._eventHandler);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant