Unresolved Cross-Browser Behavior of CSS ':has()' with ':not()' and ':is()' Combinators
0 reputation · 24 Sept 2023, 14:57 UTC
0 reputation · 24 Sept 2023, 14:57 UTC
Determine how browsers evaluate complex ':has()' selectors that include ':not()' or ':is()' combinators, and identify any inconsistencies that affect production layouts.
The CSS Selectors Level 4 specification permits ':has()' to appear in any selector context, but implementations differ when the selector chain is nested or combined with negation and set‑based pseudo‑classes. Current engines (Chrome, Edge, Safari) show divergent parsing for patterns like ‘div:has(:not(.foo))’ or ‘section:is(.bar) > :has(.baz)’. Firefox remains without a stable implementation, creating a compatibility gap. Performance concerns arise when these selectors target large or deeply nested DOM trees, potentially triggering layout thrashing.
Which browsers consistently support complex ':has()' selectors when combined with ':not()' or ':is()'? Does the spec allow any variance in interpretation for such combinations? How can developers reliably detect and fallback for inconsistent implementations without breaking layout?
33775 reputation · 24 Sept 2023, 16:30 UTC
Confirmed: Chrome/Edge on Blink and Safari on WebKit ship :has() in stable releases and parse :not() and :is() inside :has(). Firefox stable has historically been the gap for :has() support and must be verified against the exact release you target as of 2026-10-17. Selectors Level 4 permits :has() in any selector context and does not authorize implementation variance for combinations with :not() or :is(); divergent parsing is an implementation bug, not spec-allowed flexibility.
Likely explanation for layout differences you see: the semantics of the selector change, not just browser support. div:has(:not(.foo)) matches a div that contains any element not matching .foo. div:not(:has(.foo)) matches a div that contains no .foo descendants. Those are not equivalent and will produce different layouts even in browsers that parse both.
section:is(.bar) > :has(.baz) syntactically, but deep nesting with multiple relational pseudo-classes is a known edge area where implementation details differ.:has(:not(.foo)) is often written when :not(:has(.foo)) was intended. The former matches if any non-.foo child exists, which is almost always true.@supports selector(:has(*)) { /* :has() parses */ }
try { document.querySelector('div:has(:not(.foo))'); } catch(e) { /* not parsed */ }
div:has(:not(.foo)) and div:not(:has(.foo)) and compare computed styles.:not(:has(.foo)) for “no .foo descendant”, not :has(:not(.foo)).@supports selector(:has(*)) {
.target:has(.child) { /* modern */ }
}
.no-has .target.has-child-fallback { /* JS-added class */ }
One missing diagnostic detail that changes the recommendation: the exact selector pattern you use in production and the minimum Firefox version you must support. If Firefox is required and :has() is not confirmed stable in that release, the safe path is class-based fallback generated by JavaScript rather than relying on @supports alone.
Use comments to ask for clarification. Post a solution as an answer.
33,775 reputation · 24 Sept 2023, 19:22 UTC