The CSS feature :has()
is a very powerful addition to selectors. This article is about the evolution of feature detection code that is production ready!
A developer told me he was unable to properly detect the support for CSS :has()
and hence wouldn’t want to use it. The context was a fix for an issue by which a parent element should have a different size based on the presence of a certain child element. This sounded to me the perfect situation for the use of :has()
, so I didn’t want to agree with him immediately.
I started out with a nice solution, but it was quite verbose, and while implementing it, I discussed the feature detection code with a different developer, which lead to a follow up implementation. This in turn made me realize I could approach the feature detection in a different way too, which eventually lead to a simple end result that is production ready.
The following code samples basically show the evolution of a feature detection script. I know some are blunt or simplistic, and would I have better read MDN I would have turned to the end solution immediately, but it is always nice to reflect on your own line of thought.
(function () {
const s = document.createElement('style');
s.textContent = `.supportsHas:has(p):after {content: "hasHas";}`;
document.head.appendChild(s);
const supportsHas = document.createElement('div');
supportsHas.classList.add('supportsHas');
const p = document.createElement('p');
supportsHas.appendChild(p);
document.body.appendChild(supportsHas);
if (getComputedStyle(document.body, 'after')?.content === '"hasHas"') {
console.log('supports :has()');
} else {
console.log('does not suport :has()');
}
document.head.removeChild(s);
document.body.removeChild(supportsHas);
})();
The above sample (exhibit A) is an IIFE (Immediately Invoked Function Expression), which can be wrapped around any of the following pieces of code, but is not a necessity. Within the IIFE, we create a style element containing CSS that makes use of :has()
on an :after
pseudo-element, adding content to it via the content
property. This style element is injected into the document.head
.
Afterwards I create a DOM snippet that would match the CSS from the style element and inject that into the DOM too. Using getComputedStyle
, I retrieve the current state of the DOM and check whether the content property indeed contains hasHas
.
Eventually we clean up the DOM, as we know whether :has()
is supporeted or not and we do not need the injected code any more. This code is quite extensive, it’s a bit too much. It works, but we can do better.
After discussion of the above code, a developer told me I’d better query the :after
pseudo element to read its content property, instead of using getComputedStyle
, as the latter causes a reflow / recalculation. But document.querySelector
cannot be used for querying pseudo elements, so this was a no-go. Though his suggestion actually lead me to the following code (exhibit B), which is way simpler:
(function () {
window.supportsCSSHas = false;
try {
document.querySelector('body:has(div)');
console.log('supports :has()');
window.supportsCSSHas = true;
} catch {
console.log('does not support :has()');
}
})();
Before actually querying the DOM, the browser will verify the contents of the query supplied, in order to see if it is proper CSS. If a browser does not support :has()
, the query will be invalid and the browser will throw an error. Relying on try {} catch {}
here makes it simple to silently detect the :has()
feature.
Doing a little code golfing on the above leads to the following minimal detection code (exhibit C):
(function (w, d, p) {
w[p] = !1;
try {
d.querySelector('html:has(head)');
w[p] = !0;
} catch {}
})(window, document, '_supportsCSSHas');
Using html:has(head)
is the most robust, as all browsers will create a minmal DOM tree even when the response of a URL is empty.
When you use above code sample exhibit C in combination with recent CSS @supports
addition, one can make a robust feature detection and fallback solution (exhibit D):
@supports selector(A:has(b)) {
html:has(body) {
color: #fff; /* or some other styling of course*/
}
}
The essential part of exhibit D is its first line: one can check the support of a selection feature by wrapping your selector in the selector()
function using placeholder elements (kind of generics).
(function (d, c) {
try {
d.querySelector('html:has(head)') && d.documentElement.classList.add(c);
} catch {}
})(document, 'hasHas');
/* this CSS will by applied in Browsers that do support :has() */
@supports selector(A:has(b)) {
html:has(body) {
color: #fff; /* or some other styling of course */
}
}
/* this is the CSS for browsers that do not support :has() */
html.hasHas body {
color: #fff;
}
In conclusion: since not all browsers support :has()
, it is good to rely on feature detection. By discussing my intial solution I came to new insights about the approach to follow and ended up with a production ready solution.