-
Notifications
You must be signed in to change notification settings - Fork 715
[cssom] ComputedStyleObserver
to observe changes in elements' computed styles
#8982
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
Comments
It would be quite useful to use this to reflect css state (combination of media/preference/container query and :hover/:focus etc) back into js by observing a custom property. Currently those either need to be tracked in parallel in the js (makes it hard to change anything) or handled entirely in js and have the js reflect the changes to css by adding and removing classes (annoying since it would be much easier to express in css) |
I guess simple cases could be covered by the already approved #6205? element.firstChild.matchContainer("style(property: value)")).addListener(listener) But yeah not the best API if the element doesn't have children or for properties with a non-trivial value space. |
ComputedStyleObserver
to observe changes in elements' computed styles
Here's a related issue describing a problem that would need to be solved for The more of these we add (which I think would be super useful) the more this problem will become pronounced. Adding Alternatively |
+1 for this proposal. |
https://static-misc-4.glitch.me/custom-property-observer/ - here's a little hack that uses style queries and resize observer to get close to this feature. |
Another approach is https://github.com/fluorumlabs/css-variable-observer which I found a while ago. It abuses the Demo using it: https://codepen.io/bramus/details/gOEoKrL (click the page to change the values of the custom props; the Downside: the library only supports numbers because of how |
Huh, could some of the discrete value animation stuff be used to make this work for non-numbers? |
Oh, Jake, that’s the clue I needed! Here, a quick POC: https://codepen.io/bramus/pen/ZEdjjPw/1fa07805a7d6a6ea204decce87ee6705 I’m storing the value in Blogpost incoming … Update: Meh, doesn’t work in Firefox nor Safari 😭 |
Surprised this doesn't work in Safari 18, given that it supports allow-discrete. |
It‘s debatable because the computed value of (I don’t know whether a transition event should fire when the inputs change, or only when the output changes. If it’s the former, then it’s a WebKit bug) Circling back, I tried another approach: directly transition the custom properties with
While this works in Safari Technology Preview (yay!) it only partially works in Firefox (the I’ve got bugs to file … |
Building upon Jake’s suggestion to use It’s a Would still love to see something built into the CSS though, as this library zaps any of the existing transitions applied onto the element. |
Not to bring down the hype here, but I feel like one of the most important questions around this proposal is about the timing of the API. Currently all the "things" that have been built as "ponyfills" seem to be built on CSS transition events, but if the changes to the style have been done after the step 11 of of update the rendering (i.e in a fullscreenchange callback, an animation frame callback, or a ResizeObserver callback), CSS transitions would fire one rendering frame too late, with the full change well painted on screen. I'm quite doubtful this would be acceptable for many use cases. Assuming its name is really |
Agreed. I don't think this brings down the hype.
Note that: el.style.setProperty('--foo', 'bar'); …doesn't trigger a recalc. In normal circumstances, a recalc won't happen until the render steps. Calling something that reads layout ( Let's say you had an observer that tried to 'prevent' the change of console.log(el.style.getProperty('--foo')); // Still 'bar'
console.log(getComputedStyle(el).getProperty('--foo')); // Still 'bar' Even though Because of this, it seems better to fire the observer synchronously during recalc, which is already debounced. Or, accept that it's async and fire it in the render steps. |
It's not going to prevent the recalc, but it's going to prevent the changes from being painted. I believe that's what one would want to prevent, I don't think one would want to prevent a recalc through this API, just like one doesn't prevent the mutation from happening in a MutationObserver. I guess my use of the word "preventing" is to blame here, "revert" is probably more what I had in mind. Firing sync seems problematic because then you get outer scripts running in the middle of another script entirely, possibly causing many useless recalcs. For instance, one can assume that when they do trigger a full synchronous reflow in their code, they can continue requesting the computed styles again and again without triggering a new recalc (remember that it's the getters on the CSSStyleDeclaration that do trigger reflows, not to mention that the actual triggers are numerous). Firing the observer's callback synchronously in the middle of it would cause yet another reflow at the next property read (and I guess prevent some code optimization). Firing it in its own step is also problematic since just like the CSS transition trick, it will inevitably miss changes that will end up painted. |
Right, but debouncing to the render steps achieves the same, and happens less often.
I don't understand this point. Can you give a bit more detail on how debouncing this to the render steps would inevitably lead to changes being missed? |
Because we end up once more in the race for "the closest to the painting" that trukstr implied to in #8982 (comment) , and once more it won't take long before it's outraced by yet another API. Firing in the actual recalc seems like the best place to me, moreover since it's generally already debounced anyway as you noted. |
Ahhh so the problem is: If style observers fire before resize observers, then the resize observers may change styles in ways that the style observers would want to prevent rendering, but now can't. And if the ordering is reversed it's the same problem but for resize observers. Yeah, that makes sense. Thanks for explaining it. I still feel this could be limited to the rendering steps. I don't see the advantage of firing it for recalcs that happen outside the rendering steps. |
Timing problemIsn't a resize callback ultimately due to a recalc that a style observer would also be able to observe? Because of this, I think ResizeObservers and ComputedStyleObservers would have to be necessarily interleaved, or else we will have even more excrutiatingly painful observer callback ordering problems. Maybe this at least: recalc -> queue style observers -> queue resize observers such that they fire together (style and resize observers in the same task). But a microtask per recalc for style observers might be the most granular, like MutationObserver (and then style observers might even replace resize observers in some use cases because now we can rely on There is one critical problem with ResizeObserver callbacks that we would not want to replicate with ComputedStyleObserver: when changes happen inside of the RO callbacks, they often queue new calbacks for the next frame (the next render steps frame), which is undesirable currently because we have zero control over it. If this happens to ComputedStyleObserver too (callbacks being queued for the next frame instead of the current frame and hence not firing before the current render steps paint) it would completely defeat the purpose, introducing visual glitches to end users that we could otherwise avoid. Interim solution (for all observers)This section is slightly unrelated, as the next examples are for ResizeObserver, but generally speaking the timing issues (which may apply with ComputedStyleObserver) might be alleviated with the following solution. Maybe it belongs in a separate thread, but at the same time it is important to consider it for any new observer APIs such as a hypothetical ComputedStyleObserver. The simplest solution to start with for alleviating any observer timing issues is simply providing The
This would allow use cases like @Kaiido's such as reverting style changes right before paint. Most importantly we want to be able to have a definite moment at which point we can render to a canvas at the very end of render steps of the current frame (our own "paint", before the browser's actual paint). With ResizeObserver, this is practically impossible. How do you run arbitrary code after the last ResizeObserver callback of the current frame? There's no way to way to know which callback is the last one before the browser will paint, so the only options to handle "custom painting" so far are:
To prove the impossibility of solving this properly (single callback between ResizeObserver callbacks and browser paint in any frame), try solving this challenge: https://codepen.io/trusktr/pen/raBjrvb?editors=0010 You will probably need to monkey patch Without a solution, there's absolutely no way to avoid lag "glitches". With ResizeObservers in particular, this "glitch" is unavoidable unless we get Because of the above, I believe that the best and quickest solution would be to introduce If rPF example: requestPaintFrame(() => {
handleRecords()
// ...finally (finally!) "paint" to canvas here...
// ...do not perform any more DOM mutations here, as they may go to the next frame...
})
function handleRecords() {
const records = anyObserver.takeRecords()
// ...do anything you need with the records...
handleRecords() // potentially handle more records if more things changes, possibly looping forever
} Now regarding the comment The documentation for I do really like the idea of having a useful solution now, and then further observer APIs with timings thought out, with an ultimate escape hatch. |
Here's a prime example of current timing issues with observer callbacks: That issue was unjustly closed because the closer thinks there's no problem to be solved, but rather that I don't know how to use web APIs, which I consider rude on that person's part (sorry, but I'll stand up for myself all other web users who suffer from these pains forever). |
It seems reasonable to me at least that this could run either immediately before, or immediately after ResizeObserver timing. I've tried to ingest and understand everything in this thread but I can't see any particularly strong objections to it running before ResizeObservers, is there? |
@keithamus if the callback is ran only once per frame you will not be able to handle all the occurrences before they are painted. So I personally believe this needs to act like |
We need an API to observe computed style changes.
One example use case is to update canvas rendering based on changes in computed styles of an element (f.e.
transform
values to determine where to draw things in a canvas).This would be generally useful for implementing things that CSS cannot do, or making early polyfills of new CSS features that haven't landed in browsers yet.
What could it look like? Taking a note from existing observers:
Some things like callback timing need to be ironed out. Maybe its timing would be aligned with animation frames like ResizeObserver.
Existing ideas and conversation:
The main take aways from these are:
While we're making a new API, we can also see about avoiding the same problem that ResizeObserver has:
The problem is that rendering loops typically redraw in animation frame callbacks, but if CSS style change callbacks run after rAF, rendering can be delayed and incorrect and it isn't obvious why.
Maybe the initial set of callbacks for change entries should fire before rAF? Giving typical rendering setups a chance to usually be correct and only sometimes needing an additional next frame for cases when a frame changes style. I'm not totally sure what the solution should be, but it has been a pain debugging these sorts of issues.
The text was updated successfully, but these errors were encountered: