How Does the CSS :has Pseudo-Class Work?
The :has() pseudo-class is a relational selector that
enables developers to style an element based on its descendants, child
elements, or following siblings. Often referred to as the long-awaited
"parent selector," :has() checks whether an element
contains or is adjacent to specific targets before applying styles. This
article explores how :has() functions, breaks down its
syntax and specificity rules, and highlights practical design patterns
that previously required JavaScript.
Understanding Relational Selection
Traditionally, CSS cascade logic flows in one direction: top-down and
left-to-right. Standard selectors let you target an element's children
(parent > child) or subsequent siblings
(item + item), but selecting a container based on what it
contains was impossible using pure CSS.
The :has() pseudo-class fundamentally alters this
dynamic. It accepts a relative selector list as an argument and matches
the subject element if any of the selectors in the argument match
relative to that subject.
/* Styles the <article> ONLY if it contains an <img> */
article:has(img) {
display: grid;
grid-template-columns: 1fr 2fr;
}In the example above, article is the target being
styled, not the img. The browser evaluates the argument
img relative to article. If true, the grid
layout applies to the article container.
Key Syntax Patterns
The :has() selector supports complex combinators,
multiple conditions, and logical chains.
1. Direct Child Matching
Using the child combinator (>) inside
:has() ensures styles only apply when the target is an
immediate child rather than a deeply nested descendant.
/* Targets a card only if a header is its direct child */
.card:has(> h2) {
border-top: 4px solid var(--accent-color);
}2. Sibling Relationships
Because :has() accepts relative combinators, you can
evaluate adjacent siblings (+) and general siblings
(~).
/* Styles a label when its immediately preceding checkbox is checked */
label:has(+ input:checked) {
font-weight: 700;
color: var(--primary);
}
/* Styles an H2 only if it is immediately followed by a subtitle paragraph */
h2:has(+ p.subtitle) {
margin-bottom: 0.25rem;
}3. Logical Operations and Inversion
:has() can be combined with :not() and
:is() to construct sophisticated state-based styling
logic.
/* Matches a form group that has an invalid input but is NOT focused */
.form-group:has(input:invalid):not(:focus-within) {
border-color: var(--error-red);
}
/* Matches a container that contains either a video or an audio player */
.media-wrapper:has(video, audio) {
background-color: var(--dark-bg);
}Practical Use Cases
Dynamic Component Layouts
Components often require different visual treatments depending on
their internal contents. Without :has(), developers
typically toggle modifier classes like .card--has-image
using client-side scripts. With :has(), CSS handles this
declaratively:
.card {
padding: 1.5rem;
background: white;
}
/* Remove top padding if the hero image sits at the top */
.card:has(> img:first-child) {
padding-top: 0;
}Real-Time Form State Feedback
You can apply global layout adaptations when specific form controls change state.
/* Highlight a submit button when all required inputs inside the form are valid */
form:has(input:required:valid) button[type="submit"] {
background-color: var(--success-green);
cursor: pointer;
}Specificity and Performance Considerations
Understanding how :has() interacts with the CSS cascade
ensures predictable style sheets.
- Specificity Calculation: The specificity of
:has()is determined by the most specific selector inside its argument list, not the:has()keyword itself. For instance,section:has(#main-title)carries the specificity of one element (section) plus one ID (#main-title). - Nesting Restrictions: An
:has()pseudo-class cannot be nested inside another:has()pseudo-class (e.g.,:has(:has(p))is invalid). - Browser Performance: Modern rendering engines
optimize
:has()by evaluating selectors against localized DOM subtrees. To maintain optimal rendering speeds, keep the relative selector list focused and avoid attaching overly broad arguments likebody:has(*)across large DOM structures.