Late last week, I posted a tiny hack related to :has() and Firefox. This was, in some ways, a mistake. Let me explain how.
Primarily, I should have filed a bug about it. Someone else did so, and it’s already been fixed. This is all great in the wider view, but I shouldn’t be offloading the work of reporting browser bugs when I know perfectly well how to do that. I got too caught up in the fun of documenting a tiny hack (my favorite kind!) to remember that, which is no excuse.
Not far behind that, I should have remembered that Firefox only supports :has() at the moment if you’ve enabled the layout.css.has-selector.enabled flag in about:config. Although this may be the default now in Nightly builds, given that my copy of Firefox Nightly (121.0a1) shows the flag as true without the Boldfacing of Change. At any rate, I should have been clear about the support status.
Thus, I offer my apologies to the person who did the reporting work I should have done, who also has my gratitude, and to anyone who I misled about the state of support in Firefox by not being clear about it. Neither was my intent, but impact outweighs intent. I’ll add a note to the top of the previous article that points here, and resolve to do better.
I’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!
Not quite a year ago, I published an exploration of how I used layered backgrounds to create the appearance of a single bent line that connected one edge of the design to whichever navbar link corresponded to the current page. It was fairly creative, if I do say so myself, but even then I knew — and said explicitly! — that it was a hack, and that I really wanted to use anchor positioning to do it cleanly.
Now that anchor positioning is supported behind a developer flag in Chrome, we can experiment with it, as I did in the recent post “Nuclear Anchored Sidenotes”. Well, today, I’m back on my anchor BS with a return to that dashed navbar connector as seen on wpewebkit.org, and how it can be done more cleanly and simply, just as I’d hoped last year.
First, let’s look at the thing we’re trying to recreate.
To understand the ground on which we stand, let’s make a quick perusal of the simple HTML structure at play here. At least, the relevant parts of it, with some bits elided by ellipses for clarity.
Inside that (unclassed! on purpose!) <ul>, there are a number of list items, each of which holds a hyperlink. Whichever list item contains the hyperlink that corresponds to the current page gets a class of currentPage, because class naming is a deep and mysterious art.
To that HTML structure, the following bits of CSS trickery were applied in the work I did last year, brought together in this code block for the sake of brevity (note this is the old thing, not the new anchoring hotness):
If you’re wondering what the heck is going on there, please feel free to read the post from last year. You can even go read it now, if you want, even though I’m about to flip most of that apple cart and stomp on the apples to make ground cider. Your life is your own; steer it as best suits you.
Anyway, here are the bits I’m tearing out to make way for an anchor-positioning solution. The positioning-edge properties (top, etc.) removed from the second rule will return shortly in a more logical form.
That pulls out not only the positioning edge properties, but also the background dash variables and related properties. And a whole rule to relatively position the currentPage list item, gone. The resulting lack of any connecting line being drawn is perhaps predictable, but here it is anyway.
With the field cleared of last year’s detritus, let’s get ready to anchor!
Step one is to add in positioning edges, for which I’ll use logical positioning properties instead of the old physical properties. Along with those, a negative Z index to drop the generated decorator (that is, a decorative component based on generated content, which is what this ::before rule is creating) behind the entire set of links, dashed borders along the block and inline ends of the generated decorator, and a light-red background color so we can see the decorator’s placement more clearly.
I’ll also give the <a> element inside the currentPage list item a dashed border along its block-end edge, since the design calls for one.
nav.global ul li.currentPage a {
padding: 0;
padding-block: 0.25em;
margin: 1em;
color: inherit;
border-block-end: 1px dashed;
}
And those changes give us the result shown here.
Well, I did set all the positioning edge values to be 0, so it makes sense that the generated decorator fills out the relatively-positioned <div> acting as its containing block. Time to fix that.
What we need to do give the top and right — excuse me, the block-start and inline-end — edges of the decorator a positioning anchor. Since the thing we want to connect the decorator’s visible edges to is the <a> inside the currentPage list item, I’ll make it the positioning anchor:
nav.global ul li.currentPage a {
padding: 0;
padding-block: 0.25em;
margin: 1em;
color: inherit;
border-block-end: 1px dashed;
anchor-name: --currentPageLink;
}
Yes, you’re reading that correctly: I made an anchor be an anchor.
(That’s an HTML anchor element being designated as a CSS positioning anchor, to be clear. Sorry to pedantically explain the joke and thus ruin it, but I fear confusion more than banality.)
Now that we have a positioning anchor, the first thing to do, because it’s more clear to do it in this order, is to pin the inline-end edge of the generated decorator to its anchor. Specifically, to pin it to the center of the anchor, since that’s what the design calls for.
Because this anchor() function is being used with an inline inset property, the center here refers to the inline center of the referenced anchor (in both the HTML and CSS senses of that word) --currentPageLink, which in this particular case is its horizontal center. That gives us the following.
The next step is to pin the top block edge of the generated decorator with respect to its positioning anchor. Since we want the line to come up and touch the block-end edge of the anchor, the end keyword is used to pin to the block end of the anchor (in this situation, its bottom edge).
Since the inset property in this case is block-related, the end keyword here means the block end of the anchor (again, in both senses). And thus, the job is done, except for removing the light-red diagnostic background.
Once that red background is taken out, we end up with the following rules inside the media query:
The inline-start and block-end edges of the generated decorator still have position values of 0, so they stick to the edges of the containing block (the <div>). The block-start and inline-end edges have values that are set with respect to their anchor. That’s it, done and dusted.
…okay, okay, there are a couple more things to talk about before we go.
First, the dashed borders I used here don’t look fully consistent with the other dashed “borders” in the design. I used actual borders for the CSS in this article because they’re fairly simple, as CSS goes, allowing me to focus on the topic at hand. To make these borders fully consistent with the rest of the design, I have two choices:
Remove the borders from the generated decorator and put the background-trick “borders” back into it. This would be relatively straightforward to do, at the cost of inflating the rules a little bit with background sizing and positioning and all that.
Convert all the other background-trick “borders” to be actual dashed borders. This would also be pretty straightforward, and would reduce the overall complexity of the CSS.
On balance, I’d probably go with the first option, because dashed borders still aren’t fully visually consistent from browser to browser, and people get cranky about those kinds of inconsistencies. Background gradient tricks give you more control in exchange for you writing more declarations. Still, either choice is completely defensible.
Second, you might be wondering if that <div> was even necessary. Not technically, no. At first, I kept using it because it was already there, and removing it seemed like it would require refactoring a bunch of other code not directly related to this post. So I didn’t.
But it tasked me. It tasked me. So I decided to take it out after all, and see what I’d have to do to make it work. Once I realized doing this illuminated an important restriction on what you can do with anchor positioning, I decided to explore it here.
As a reminder, here’s the HTML as it stood before I started removing bits:
Originally, the <div> was put there to provide a layout container for the logo and navbar links, so they’d be laid out to line up with the right and left sides of the page content. The <nav> was allowed to span the entire page, and the <div> was set to the same width as the content, with auto side margins to center it.
So, after pulling out the <div>, I needed an anchor for the navbar to size itself against. I couldn’t use the <main> element that follows the <nav> and contains the page content, because it’s a page-spanning Grid container. Just inside it, though, are <section> elements, and some (not all!) of them are the requisite width. So I added:
main > section:not(.full-width) {
anchor-name: --mainCol;
}
The full-width class makes some sections page-spanning, so I needed to avoid those; thus the negative selection there. Now I could reference the <nav>’s edges against the named anchor I just defined. (Which is probably actually multiple anchors, but they all have the same width, so it comes to the same thing.) So I dropped those anchor references into the CSS:
And that worked! The inline start and end edges, which in this case are the left and right edges, lined up with the edges of the content column.
…except it didn’t work on any page that had any content that overflowed the main column, which is most of them.
But wait. Why couldn’t I just position the logo and list of navigation links against the --mainCol anchor? Because in anchored positioning, just like nearly every other form of positioning, containing blocks are barriers. Recall that the <nav> is a fixed-position box, so it can stick to the top of the viewport. That means any elements inside it can only be positioned with respect to anchors that also have the <nav> as their containing block.
That’s fine for the generated decorator, since it and the currentPageLink anchor both have the <nav> as their containing block. To try to align the logo and navlinks, though, I can’t look outside the <nav> at anything else, and that includes the sections inside the <main> element, because the <nav> is not their containing block. The <nav> element itself, on the other hand, shares a containing block with those sections: the initial containing block. So I can anchor the <nav> itself to --mainCol.
I fiddled with various hacks to extend the background of the <nav> without shifting its content edges, padding and negative margins and stuff like that, but in end, I fell back on a border-image hack, which required I remove the background.
Was it worth it? I have mixed feelings about that. On the one hand, putting all of the layout hackery into the CSS and removing it all from the HTML feels like the proper approach. On the other hand, it’s one measly <div>, and taking that approach means better support for older browsers. On the gripping hand, if I’m going to use anchor positioning, older browsers are already being left out of the fun. So I probably wouldn’t have even gone down this road, except it was a useful example of how anchor positioning can be stifled.
At any rate, there you have it, another way to use anchor positioning to create previously difficult design effects with relative ease. Just remember that all this is still in the realm of experiments, and production use will be limited to progressive enhancements until this comes out from behind the developer flags and more browsers add support. That makes now a good time to play around, get familiar with the technology, that sort of thing. Have fun with it!