0

Here's a simplified structure of a web page.

<div>
    <div>div 1</div>
    <div>div 2<a href="/foo">foo</a></div>
    <p>select me</p>
    <p>keep me alone</p>
</div>

I want to select all p based on it's neighbour div with a inside. Here's my best attempt:

document.querySelectorAll('(div > a[href="/foo"]) ~ p')

But obviously this is not a vaild CSS selector.

Is it possible to select p if it has a neighbour div with a inside?

Eugene Karataev
  • 1,777
  • 1
  • 11
  • 24
  • You could do this using `:has()` - but `:has()` only works inside `querySelector`/`querySelectorAll` - it cannot be used in stylesheets. Where are you wanting to use this selector? (As of early 2020, no browser supports `:has()` but there are polyfills for it). – Dai May 27 '20 at 02:32
  • I want to use the selector in devtools. Is it possible to inject the polyfill in an arbitrary webpage and use `querySelector` with `:has()` polyfilled? – Eugene Karataev May 27 '20 at 03:01

3 Answers3

2

Is it possible to select p if it has a neighbour div with a inside?

You would need to use the :has() selector, however the full solution depends if you want to use it in a "live profile" or a "snapshot profile":

The live profile is appropriate for use in any context, including browser CSS selector matching, which is live. It includes every selector defined in this document, except for :has():

The snapshot profile is appropriate for contexts which aren’t extremely performance sensitive; in particular, it’s appropriate for contexts which evaluate selectors against a static document tree. For example, the querySelector() method defined in DOM should use the snapshot profile.

When talking about :has() specifically:

  • In .css/<style> stylesheet rule selectors (which is a live context): No.
  • In .css/<style> stylesheet rule selectors with client-side scripting to allow the use of :has() (which means it actually is used a snapshot context): Yes
  • In querySelector, querySelectorAll, matches and other DOM functions (which is a snapshot context): Yes
    • But as of May 2020 no browser natively supports using :has() in these functions.
    • And after some cursory googling, I couldn't find a polyfill or patch to add support for :has() for querySelector etc, boo!

But assuming you can use :has() (e.g. because you're reading this post in the year 2030), then this query is all you need:

div:has(> a[href="/foo"]) ~ p

Why isn't :has() in the Live Profile?

There are a variety of reasons why CSS does not support selecting elements based on its children (though there are a limited number of exceptions such as :empty and :focus-within), but the main reasons are:

  • It would make CSS selectors even more computationally expensive to evaluate.
  • CSS is designed to work with a partially loaded document, such that as a document is loaded it won't drastically change appearance.

As a thought-experiment: pretending that browsers did support :has() in a CSS style rule, consider a HTML document that looks like this:

<html>
<head>
    <style>
    p { background-color: red; }
    p:has(div) { background-color: blue; }
    </style>
</head>
<body>
    <p>foo <div></div></p>
    <p>bar</p>
    <p>baz <div></div></p>
</body>
</html>

The "foo" and "baz" elements would be blue, and the "bar" element would be red.

Now if a browser has this document in a partially-loaded state, such that it only has this loaded so far (before the user's Wi-Fi connection died) - or before the next TCP packet arrives:

<html>
<head>
    <style>
    p { background-color: red; }
    p:has(div) { background-color: blue; }
    </style>
</head>
<body>
    <p>foo

Browsers are required to cleanly terminate a partially-loaded document by inserting their own closing-tags for truncated elements, like so:

<html>
<head>
    <style>
    p { background-color: red; }
    p:has(div) { background-color: blue; }
    </style>
</head>
<body>
    <p>foo</p>
</body>
</html>

So when the document is in this state, the first p would be red, not blue - then after the user's Wi-Fi connection reconnects (or the next TCP packet arrives) and the page finishes downloading then <p>foo</p> gains the <div> and so the :has(div) rule now applies and the <p>foo</p> will suddenly change from red to blue - which isn't good.

Now in this contrived example, a trivial difference like a background-color change doesn't matter all that much - but if the :has() selector was used to control some major differences in page layout (e.g. body:has(footer) > main { display: grid;) then that would really mess-up the page layout while it's still loading - and this is why selectors based on descendant content will not be supported for now in their current form.

(But given the sheer practicality and utility of parent-of-descendant selectors, I imagine they'll be reintroduced in the future with some way of restricting rules for use only after DOMContentLoaded has fired or requiring a default or fallback for when a selector is indeterminate.

Community
  • 1
  • 1
Dai
  • 141,631
  • 28
  • 261
  • 374
  • My goal is to query elements on a webpage with devtools. Am I correct that it's not possible with `:has()`, because it's not supported yet? https://caniuse.com/#search=%3Ahas – Eugene Karataev May 27 '20 at 02:58
  • Thanks a lot for the detailed answer! I now have a better picture of what is possible and what is not in CSS in 2020. I need to re-think my strategy of querying elements, thanks! – Eugene Karataev May 27 '20 at 03:20
0

I may not be understanding your question correctly, however if you wanted to just select <p>select me</p>, why can you not just assign it to a class?

For example,

HTML:

<div>
    <div>div 1</div>
    <div>div 2<a href="/foo">foo</a></div>
    <p class="selected">select me</p>
    <p>keep me alone</p>
</div>

CSS:

.selected {
color: green;
}

Zach P.
  • 342
  • 1
  • 10
-1

As of now (as for as I know) you can't select a parent from a child in CSS, let alone and adjacent "parent". The best answer I have for you is this though, hopefully it helps.

div + p {
  background-color: yellow;
}
<div>
    <div>div 1</div>
    <div>div 2<a href="/foo">foo</a></div>
    <p>select me</p>
    <p>keep me alone</p>
</div>
John
  • 5,132
  • 1
  • 6
  • 17
  • You can use selectors that use "X contains Y"-type logic, but only inside `querySelector` and `querySelectorAll` calls - and not inside stylesheets. – Dai May 27 '20 at 02:30
  • @Dai yes of course you can do a lot with JS to do that, but I believe it's not possible with only CSS which I think is what the OP was asking. – John May 27 '20 at 02:32
  • Very cool, but still requires JS to perform. Thanks for sharing though. I didn't know about this. – John May 27 '20 at 02:38
  • Thanks for the answer. Unfortunately, the webpage I'm interested in has much more complex structure and `div + p` selector will query elements I'm not interested in. I need to cling to the `a` inside the `div` to get the elements I want. – Eugene Karataev May 27 '20 at 03:08
  • 1
    I figured as much, I don't think there is a way to do it in just CSS though unfortunately, You'll probably need some JS to achieve what you want. – John May 27 '20 at 03:14