Prodding Firefox to Update :has() Selection
Published 1 year, 1 month pastI’ve posted a followup to this post which you should read before you read this post, because you might decide there’s no need to read this one. If not, please note that what’s documented below was a hack to overcome a bug that was quickly fixed, in a part of CSS that wasn’t enabled in stable Firefox at the time I wrote the post. Thus, what follows isn’t really useful, and leaves more than one wrong impression. I apologize for this. For a more detailed breakdown of my errors, please see the followup post.
I’ve been doing some development recently on a tool that lets me quickly produce social-media banners for my work at Igalia. It started out using a vanilla JS script to snarfle up collections of HTML elements like all the range inputs, stick listeners and stuff on them, and then alter CSS variables when the inputs change. Then I had a conceptual breakthrough and refactored the entire thing to use fully light-DOM web components (FLDWCs), which let me rapidly and radically increase the tool’s capabilities, and I kind of love the FLDWCs even as I struggle to figure out the best practices.
With luck, I’ll write about all that soon, but for today, I wanted to share a little hack I developed to make Firefox a tiny bit more capable.
One of the things I do in the tool’s CSS is check to see if an element (represented here by a <div>
for simplicity’s sake) has an image whose src
attribute is a base64
string instead of a URI, and when it is, add some generated content. (It makes sense in context. Or at least it makes sense to me.) The CSS rule looks very much like this:
div:has(img[src*=";data64,"])::before {
[…generated content styles go here…]
}
This works fine in WebKit and Chromium. Firefox, at least as of the day I’m writing this, often fails to notice the change, which means the selector doesn’t match, even in the Nightly builds, and so the generated content isn’t generated. It has problems correlating DOM updates and :has()
, is what it comes down to.
There is a way to prod it into awareness, though! What I found during my development was that if I clicked or tabbed into a contenteditable
element, the :has()
would suddenly match and the generated content would appear. The editable element didn’t even have to be a child of the div
bearing the :has()
, which seemed weird to me for no distinct reason, but it made me think that maybe any content editing would work.
I tried adding contenteditable
to a nearby element and then immediately removing it via JS, and that didn’t work. But then I added a tiny delay to removing the contenteditable
, and that worked! I feel like I might have seen a similar tactic proposed by someone on social media or a blog or something, but if so, I can’t find it now, so my apologies if I ganked your idea without attribution.
My one concern was that if I wasn’t careful, I might accidentally pick an element that was supposed to be editable, and then remove the editing state it’s supposed to have. Instead of doing detection of the attribute during selection, I asked myself, “Self, what’s an element that is assured to be present but almost certainly not ever set to be editable?”
Well, there will always be a root element. Usually that will be <html>
but you never know, maybe it will be something else, what with web components and all that. Or you could be styling your RSS feed, which is in fact a thing one can do. At any rate, where I landed was to add the following right after the part of my script where I set an image’s src
to use a base64
URI:
let ffHack = document.querySelector(':root');
ffHack.setAttribute('contenteditable','true');
setTimeout(function(){
ffHack.removeAttribute('contenteditable');
},7);
Literally all this does is grab the page’s root element, set it to be contenteditable
, and then seven milliseconds later, remove the contenteditable
. That’s about a millisecond less than the lifetime of a rendering frame at 120fps, so ideally, the browser won’t draw a frame where the root element is actually editable… or, if there is such a frame, it will be replaced by the next frame so quickly that the odds of accidentally editing the root are very, very, very small.
At the moment, I’m not doing any browser sniffing to figure out if the hack needs to be applied, so every browser gets to do this shuffle on Firefox’s behalf. Lazy, I suppose, but I’m going to wave my hands and intone “browsers are very fast now” while studiously ignoring all the inner voices complaining about inefficiency and inelegance. I feel like using this hack means it’s too late for all those concerns anyway.
I don’t know how many people out there will need to prod Firefox like this, but for however many there are, I hope this helps. And if you have an even better approach, please let us know in the comments!
Comments (5)
“Well, there will always be a root element. Usually that will be but you never know, maybe it will be something else, what with web components and all that.”
Referencing the root element in CSS is actually quite easy, no matter whether it’s
<html>
,<xml>
, or<???>
:I see you used
:root
in your querySelector. When expressed as CSS, it’s exactly the same:… no matter what the element might happen to be.
I am slightly confused here. Firefox (release and beta) still do not support
:has()
. But I assume this is a sort of app for internal use ? And you flip the relevant about:config flag ? And:has()
is still buggy…(I really wish
:has()
was fully supported though, that would make my life easier.)Hi! I am currently working on shipping :has on Firefox (Soon!) and was shown this – The included bugzilla link tracks this as Bug 1860136, and the fix should be landing soon into Nightly.
Pingback ::
Weeknotes 23:35 - Jeff Bridgforth :: Front-end developer in Chattanooga, Tennessee | Jeff Bridgforth :: Front-end developer in Chattanooga, Tennessee
[…] Prodding Firefox to Update :has() Selection (Eric Meyer) – I wish Firefox would implement the :has selector soon. […]
I rely heavily on the :has() selector, and I didn’t realize it wasn’t supported in Firefox. Now, I’ll need to go back and adjust some styles.