CSS :has() feature detection with @supports(selector(…)): You want :has(+ *), not :has(*)

# Feature detecting :has()

To feature detect browser support for the CSS :has() selector, you can use @supports(selector(…)). When doing so, it is important to include a valid selector as its argument. As I’ve tweeted before, you must pass a selector such as * into :has() when used in a feature query.

/* ❌ This will always evaluate to false */
@supports selector(:has()) {
  …
}

/* ✅ This will evaluate to true in browsers that support :has() */
@supports selector(:has(*)) {
  …
}

💁‍♂️ Initially this selector requirement was only the case in Safari, but this has since been adjusted at the spec level making it a requirement in all other browsers.

I use this technique to conditionally show a warning in many of my :has() demos.

/* Style warning block */
.no-support {
  margin: 1em 0;
  padding: 1em;
  border: 1px solid #ccc;
  background-color: #ff00002b;
  display: block;
}

/* Hide warning block in case :has() support is detected */
@supports selector(:has(*)) {
  .no-support {
    display: none;
  }
}

If your browser has no support, you get to see a warning message telling you about it. This is the case in Firefox which, at the time of writing, does not support :has() out of the box just yet.

Screenshot of Firefox showing one of my :has() demos. As it lacks support for :has(), visitors get to see a warning message telling them the demo will not work correctly.

~

# The Problem with :has(*)

While the approach above does allow you to feature detect :has(), it is not 100% closing. The culprit here is Firefox, which currently has experimental support behind a feature flag.

When flipping the layout.css.has-selector.enabled flag on, Firefox will correctly claim support when using @supports selector(:has(*)). This, however, does not account for the fact that this experimental implementation does not support relative selector parsing (yet).

UPDATE 2023.03.07 – As of March 2023, Firefox has support for relative selectors inside :has() so regular feature detection works just fine now. However, do note that the :has() implementation in Firefox is not finalized yet, as style invalidation still needs to be tackled. See this comment for some more info.

As many of my demos use relative selectors within :has(), I do want a warning to be shown even when the flag is flipped on. The solution here is to actually use a relative selector such as + * as the argument to :has().

Like so:

/* Hide warning block in case :has() support – including relative selectors – is detected */
@supports selector(:has(+ *)) {
  .no-support {
    display: none;
  }
}

~

# Full Code

The full code used in my demo looks like this. It can detect both levels of support and also allows showing a certain box in case support is claimed.

<div class="no-support" data-support="css-has-basic"><p>🚨 Your browser does not support CSS <code>:has()</code>, so this demo will not work correctly.</p></div>
<div class="no-support" data-support="css-has-relative"><p>🚨 Your browser does not support relative selectors in CSS <code>:has()</code>, so this demo will not work correctly.</p></div>
.no-support,
.has-support {
  margin: 1em 0;
  padding: 1em;
  border: 1px solid #ccc;
}

.no-support {
  background-color: #ff00002b;
  display: block;
}
.has-support {
  background-color: #00ff002b;
  display: none;
}

@supports selector(:has(*)) {
  .no-support[data-support="css-has-basic"] {
    display: none;
  }
  .has-support[data-support="css-has-basic"] {
    display: block;
  }
}

@supports selector(:has(+ *)) {
  .no-support[data-support="css-has-relative"] {
    display: none;
  }
  .has-support[data-support="css-has-relative"] {
    display: block;
  }
}

~

# TL;DR

If you’re feature detecting :has() with @supports you must pass a selector into :has(). This can be * but if your code relies on relative selectors used inside :has(), use @supports selector(:has(+ *)) instead. This must be done to filter out Firefox visitors who have flipped on the experimental :has() support which currently lacks support for relative selectors.

~

# Spread the word

To help spread the contents of this post, feel free to retweet its announcement tweet:

~

Published by Bramus!

Bramus is a frontend web developer from Belgium, working as a Chrome Developer Relations Engineer at Google. From the moment he discovered view-source at the age of 14 (way back in 1997), he fell in love with the web and has been tinkering with it ever since (more …)

Unless noted otherwise, the contents of this post are licensed under the Creative Commons Attribution 4.0 License and code samples are licensed under the MIT License

Join the Conversation

4 Comments

    1. Technically it’s not a false positive as Firefox does support relative selector matching by now – see bug https://bugzilla.mozilla.org/show_bug.cgi?id=1774588, which is closed/fixed.

      However, what’s still an issue is style invalidation. Updates to the DOM or CSS don’t visually update things matched by `:has()` selectors with relative selectors.

      See my demo at https://cdpn.io/pen/debug/YzvowwJ for example. It kinda works, but you often need to force a style recalc by toggling the `.special` class off and on again – `$0.classList.toggle(‘special’); $0.offsetLeft; $0.classList.toggle(‘special’); `

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.