Use when styling an element based on a descendant, sibling, or sibling-state condition; building a parent-style-from-child relationship in CSS without JavaScript; choreographing form-state styling (`form:has(input:invalid)`); or deciding whether `:has()` or a JS class-toggle is the right tool. Prevents the four standard performance and validity traps : anchoring on `body` or `:root` (re-evaluates on every DOM mutation), placing pseudo- elements inside `:has()` (spec-prohibited), nesting `:has()` inside `:has()` (also spec-prohibited), and reaching for `:has()` when `:focus-within` already solves the problem with cheaper semantics. Covers `:has(<relative-selector-list>)` syntax, the forgiving inner list, the highest-specificity rule, restrictions, combinator placement (`> child`, `+ sibling`, `~ sibling`), OR-via-comma vs AND-via-chained- `:has(...)`, anchor-locality performance heuristics, gating via `@supports selector(:has(*))`, and Baseline status. Keywords: :has, has selector, relational selector, parent
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Use when styling an element based on a descendant, sibling, or sibling-state condition; building a parent-style-from-child relationship in CSS without JavaScript; choreographing form-state styling (`form:has(input:invalid)`); or deciding whether `:has()` or a JS class-toggle is the right tool. Prevents the four standard performance and validity traps : anchoring on `body` or `:root` (re-evaluates on every DOM mutation), placing pseudo- elements inside `:has()` (spec-prohibited), nesting `:has()` inside `:has()` (also spec-prohibited), and reaching for `:has()` when `:focus-within` already solves the problem with cheaper semantics. Covers `:has(<relative-selector-list>)` syntax, the forgiving inner list, the highest-specificity rule, restrictions, combinator placement (`> child`, `+ sibling`, `~ sibling`), OR-via-comma vs AND-via-chained- `:has(...)`, anchor-locality performance heuristics, gating via `@supports selector(:has(*))`, and Baseline status. Keywords: :has, has selector, relational selector, parent selector, sibling combinator, forgiving selector list, specificity, anchor element, @supports selector, :focus-within, page slow with :has, scroll jank, input lag, :has not matching, style based on child, want parent selector, how to style parent based on child, how do I style a parent in CSS, can I select sibling in CSS, what is the :has selector, why is :has slow.
license
MIT
compatibility
Designed for Claude Code. Requires Frontend Design evergreen-2026.
metadata
{"author":"OpenAEC-Foundation","version":"1.0"}
Frontend Syntax : CSS :has() Relational Selector
Authoritative reference for the relational pseudo-class. Replaces JS-driven parent-state classes with native CSS while avoiding the well-known performance trap.
Per evergreen-2026, :has() no longer requires a gate by default in 2026; legacy code paths that still target Newly-tier browsers SHOULD gate with @supports selector(:has(*)).
The <relative-selector-list> is evaluated against the anchor element. Each item is a relative selector; the implicit anchor is the element being tested.
Form
Example
Meaning
Descendant
.gallery:has(img.broken)
Gallery containing a broken image anywhere inside
Direct child
.gallery:has(> img)
Gallery whose direct child is an <img>
Next sibling
h2:has(+ p)
h2 immediately followed by a <p>
General sibling
.section:has(~ .footer)
.section followed (later) by .footer
OR (comma)
article:has(figure, table)
Article containing figure OR table
AND (chained)
article:has(figure):has(table)
Article containing figure AND table
Specificity rule
The specificity of :has(...) equals the HIGHEST-specificity selector inside, EXCLUDING the notation itself. Same rule as and .
:has(...)
:is()
:not()
Selector
Specificity
h1:has(+ h2)
0,0,2 (two element selectors)
article:has(.featured)
0,1,1 (.featured is the highest)
.card:has(#hero)
1,1,0 (#hero is the highest)
Forgiving list behavior
The inner list is FORGIVING : unknown selectors are skipped silently rather than invalidating the whole rule. The OUTER :has(...) itself is NOT forgiving : if the browser does not implement :has() at all, the entire rule is dropped unless wrapped in :is() or :where() :
/* Whole rule dropped on browsers without :has() */h1:has(+ h2) { ... }
/* Forgiven : the unsupported branch is ignored, h1 still styled */:is(h1:has(+ h2), h1) { ... }
Restrictions (HARD)
NEVER place a pseudo-element inside :has(). div:has(::before) is invalid; ::before cannot exist independently of its originating element for the purposes of matching.
NEVER use a pseudo-element as the anchor. ::before:has(.x) is invalid.
NEVER nest :has() inside :has(). .a:has(.b:has(.c)) is invalid.
:has() does NOT match the anchor itself. .card:has(.card) selects an outer .card that contains another .card, never the inner one against its own descendants.
Decision tree : "Is :has() the right tool here vs JS?"
Need to style a parent based on a child's PRESENCE :
-> :has(.child) (CSS-only, no JS)
Need to style a parent based on a child's STATE (:checked, :invalid, :empty) :
-> :has(input:checked) (CSS-only, no JS)
Need to style a parent based on a child's :focus :
-> :focus-within (cheaper, simpler) NOT :has(:focus)
Need to style based on a sibling's existence or state :
-> :has(+ .sibling) or :has(~ .later-sibling)
Need to react to a JS-managed state that JS already mutates (e.g. open/closed modal) :
-> Toggle a class via JS. The class is faster and more explicit.
Need to nest :has() inside :has() :
-> NEVER. Restructure the markup or compose with chained :has(...).
Need pseudo-element inside :has() :
-> NEVER. Match the originating element instead.
Decision tree : "How tight is my anchor?"
Anchor selector = body, html, :root, or *:
-> NEVER. Every DOM mutation triggers a full re-evaluation.
Anchor selector = unbounded element type (e.g. div:has(...)):
-> Tighten with a class : .panel:has(...), not div:has(...).
Anchor selector = component-root class with a small subtree:
-> Good. Constrain the inner with a combinator (> child, + sibling).
Inner selector = bare element type traversing many descendants:
-> Tighten with > or + or ~ to limit traversal depth.
Inner selector = nested deep paths (.a .b .c .d):
-> Tighten to direct relationship (> .b > .c > .d) if semantics allow.
Decision tree : "Should I gate with @supports?"
Project target = evergreen-2026 only:
-> No gate required (Widely Available reached).
Project target includes pre-2024 visitors:
-> Gate with @supports selector(:has(*)) and provide fallback.
Selector parser still fails on :has() in some user-agents:
-> Wrap in :is() or :where() to make the outer rule forgiving.
Mental Model
:has() rotates the cascade : instead of "child styled by parent", you can now write "parent styled by child". The selector evaluator first computes which elements match the inner relative selector, then reports the anchor that has at least one such descendant or sibling.
The performance characteristic is the inverse of normal selectors. A normal selector evaluates left-to-right (browser does right-to-left optimization internally, but the cost is bounded by the rightmost compound). :has() requires the engine to maintain a reverse-relationship table : for every potential anchor, track what changes to its subtree could change the match. The smaller the subtree per anchor, the cheaper the maintenance. Hence the inviolable rule : ANCHOR ON THE SMALLEST POSSIBLE SUBTREE.
body:has(...) is the canonical anti-pattern. It declares that ANY mutation anywhere in the page MAY change the match. Browsers do their best to optimize, but in practice this causes input lag during scroll, drag, and animation. Anchor on a component-root instead (.gallery:has(...), .form:has(...), .card:has(...)).
The "killer feature" status of :has() comes from two patterns that previously required JS : (1) form-state choreography, e.g. form:has(input:invalid) [type=submit] { opacity: 0.5; pointer-events: none; } enables/disables a submit button based on validation state without any JS, and (2) sibling-aware layout, e.g. h2:has(+ p) { margin-block-end: 0; } removes the bottom margin of a heading immediately followed by a paragraph.
:has() does NOT match the anchor against itself. article:has(article) selects an OUTER article that contains an INNER article; the inner one is not selected unless it also contains a third article. This is unlike :scope which can reference the matched element.
Patterns
Pattern 1 : Style a card when its button is hovered
references/examples.md : worked patterns including form-state, sibling-rhythm, container-driven layout
references/anti-patterns.md : 8 anti-patterns with symptom / root cause / fix
Note : the masterplan-listed source https://web.dev/articles/css-has returned HTTP 404 during verification on 2026-05-19. MDN and W3C Selectors Level 4 provide the same normative material; no claim in this skill depends on the web.dev page.
Cross-references
[[frontend-core-web-standards-baseline]] for @supports selector(...) gating discipline
[[frontend-syntax-css-cascade-layers-scope]] for cascade-layer placement of relational rules
[[frontend-a11y-focus-keyboard-inert]] for :focus-within semantics
[[frontend-syntax-html5-form]] for form-state choreography patterns
[[frontend-perf-animation-gpu-containment]] for anchor-locality and containment