:has() — the Parent Selector
For years, "give me a CSS selector that styles a parent based on its children" was the single most requested feature CSS didn't have. The :has() pseudo-class finally delivers it. It matches an element if, and only if, at least one element matching the relative selector passed to it exists somewhere inside that element (by default, among its descendants).
Basic syntax
/* Match a .card ONLY if it contains an <img> */
.card:has(img) {
display: grid;
grid-template-columns: 120px 1fr;
}
/* Match a heading only if it is immediately followed by a paragraph */
h2:has(+ p) {
margin-bottom: 0.5em;
}The selector inside :has() is evaluated relative to the element :has() is attached to. img inside .card:has(img) means "an img anywhere inside this .card," while + p means "a p that is the next sibling of this element."
Worked example: form validation styling
A very common real-world use case is highlighting an entire form group when one of its inputs is invalid — something that previously required JavaScript to add a class to the parent element.
.form-group {
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 6px;
transition: border-color 0.15s ease;
}
/* Style the whole group differently if it contains an
invalid, user-interacted input */
.form-group:has(:invalid) {
border-color: crimson;
background: #fff5f5;
}
.form-group:has(:invalid) label {
color: crimson;
}<div class="form-group"> <label for="email">Email</label> <input id="email" type="email" required value="not-an-email" /> </div>
Worked example: cards that adapt to their content
:has() also lets a component adapt its own layout depending on whether an optional element is present, with no extra markup or modifier classes needed.
/* Default: a text-only card */
.card {
padding: 1.5rem;
}
/* If the card happens to contain an image, switch to a
media layout automatically */
.card:has(img) {
display: grid;
grid-template-columns: 96px 1fr;
gap: 1rem;
padding: 1rem;
}
/* Cards containing a .badge get extra top padding so the
badge doesn't overlap the title */
.card:has(.badge) {
padding-top: 2.5rem;
position: relative;
}More things :has() can do
body:has(dialog[open])— style the whole page while a modal dialog is open, e.g. to disable scrolling.a:has(> img)— target links whose direct child is an image (image-only links)..field:has(input:focus)— highlight a field wrapper while its input is focused (an alternative to:focus-withinwhen you also need to check for other conditions).h1:has(+ h2)— select a heading only when a specific sibling follows it.