Author Information

Brian Kardell
  • Developer Advocate at Igalia
  • Original Co-author/Co-signer of The Extensible Web Manifesto
  • Co-Founder/Chair, W3C Extensible Web CG
  • Member, W3C (OpenJS Foundation)
  • Co-author of HitchJS
  • Blogger
  • Art, Science & History Lover
  • Standards Geek
Follow Me On...
Posted on 05/13/2021

Can I :has()

As you might know, my company (Igalia) works on all of the web engines and we contribute a lot. I'm very proud of all of the things we're able to do to improve both the features of the web platform, and the overall health of this commons. I'm especially pleased when this lets us tackle historically hard problems. A very incomplete list of things with some historical challenges that we've helped move in important ways the past few years would include: CSS Grid, MathML, JavaScript Class features, hardware accelerated SVGs and Container Queries. Today I'll be telling you about another one we're working on.

Today we're filing an intent to prototype, tackling yet another historically hard problem for the web: The :has() selector. In this post, I'd like to explain what this intent means, as well as why it matters, where it comes from and why I am very excited about it.

:has() for the unfamilliar

When you write a CSS rule, the last (right-most) bit of the selector is the thing that you're styling. We call that the "subject" of the rule. Most people writing CSS have, at some point, found themselves wanting to style something based on what is inside it. You might have heard people talk about wanting "a parent selector" or an "ancestor combinator". :has() is that - basically.

/* style an .x that contains a .y descendant - not the .y */ 
.x:has(.y) { ... }

The long history of postponed :has

The basic reasons to desire such powers are pretty obvious. Powerful selection ability greatly enables a real separation of concerns. This fact wasn't lost on anyone. XPath allowed it, and CSS specifications since the late 1990's have tried.

In fact, a lot of people learned about :has() first through jQuery. That's because when John Resig wrote it he wanted it to support all of the ""new CSS 3 selectors" - and :has() was one of them. It was in the spec, so jQuery supported the :has() selector pseudo. The trouble, of course, is that no one actually knows what will gain implementations and reach recommendations at the start of the process - and :has() didn't, and was postponed again to Selectors Level 4. The first draft of Selectors Level 4 was published in 2011. It is highly starred, was among the top requested features in Microsoft's old User Voice system, and every year remains among the top 2-3 most requested features. Every so often someone (frequently me) brings it up again in the CSS Working Group.

Why the hold up?

Primarily, even without :has() it's pretty hard to live up to performance guarantees of CSS, where everything continue to evaluate and render "live" at 60fps. If you think, mathematically, about just how much work is conceptually involved in applying hundreds or thousands of rules as the DOM changes (including as it is parsing), it's quite a feat as is.

Engines have figured out how to optimize this based on clever patterns and observations that avoid the work that is conceptually necessary - and a lot of that is sort of based on these subject invariants that has() would appear to throw to the wind.

At the same time, there are plenty of aspects of this problem that are considerably easier than others. CSS engines for print, for example support :has() because they don't need to run at 60fps. DOM APIs like querySelector() / querySelectorAll() / matches() also check at a very specific point in time in a completely different manner - it's very doable there, as jQuery showed.

There are limits that we could potentially place on this selector that might help a little. Or, there are things like :focus-within or :empty which seem similar, but internally, are very specifically easier - but very incomplete.

And so, for the last decade this comes up once or twice a year as we try to find some way forward. In the end, we ultimately sort of go around in circles: It's impossible to make decisions and progress while everything is in limbo. We don't really know what the options are, and its hard for anyone to take up trying to imagine a way forward. As we've seen with some other things, like Container Queries, this is also somewhat of a vicious cycle: The more it comes up, and the more it is discussed without ultimate progress of any real kind, the less likely it is that anyone actually wants to talk about it again.

We need prototyping, exploration, data we can point to and more concrete things we can discuss - but the longer it goes on, the more hopeless it looks and the less likely anyone is to do it.

eyeo, Igalia and prototypes

Igalia works on all of the web engines, and with lots of consumers of those engines to expand investment in this wonderful commons. eyeo makes a number of products like the Adblock Browser and Adblock Plus. While some sites can offer workarounds that employ additional classes for intentionaly styling these sorts of things, plenty of other things cannot. Lots of very useful things (reader mode, ad blockers, conformance checker plugins and search are just some examples) rely on selectors and heuristics abbout trees of markup that they didn't create. They have a definite separation of concerns and thus observe these sorts of shortcommings very acutely. Having no native solutions for some of these hard problems causes everyone to have to find their own ways deal with it themselves, and all of them have different performance characteristics and different edge cases, and all of them require additional JavaScript. That's not good for anyone. So, eyeo approached us about sponsoring work, research and prototyping on some things - among them :has(). Can we somehow get past these impasses and make progress on this one, and make things better for the entire community? What might that look like? Can it conceivably work in the main 60fps CSS? If not, can we provide some research and data that allows other paths like support in the JavaScript DOM methods, or a static profile? Let's be sure to include all of important uses of selectors.

For the past little while, Igalia engineers have been looking into this problem. We've been having discussions with Chromium developers, looking into Firefox and WebKit codebases and doing some initial protypes and tests to really get our heads around it. Through this, we've provided lots of data about performance of what we have already and where we believe challenges and possibilities lie. We've begun sketching out an explainer with all of our design notes and questions linked up - so it's all there in the open for people to review as we attempt to open this discussion.

Today's intent: What it means

The meaning of "intents" have been occasionally difficult for the larger community to understand in the same way, so I wanted to take a moment to suggest how to interpret it: With today's intent, we're simply stating that we feel that we have gathered enough information and data on this that we feel like we're ready to share it for wider review and discussion, productively. We believe that the data suggests that it seems at least plausible to carry on with discussions around supporting a (partially limited) form of :has in the main, live CSS. We would like for data, designs and limits to be discussed fairly concretely. We would like like to carry forward with additional, concrete implementation prototyping and continue to help sort out a path forward.