Lesson 11 of 30
CSS selectors have only ever been able to look at an element's own state or what came before it. :has() breaks that rule — it lets a selector match based on what's inside or after it.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
Given `.card:has(img)`, which element does the rule actually style?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Every CSS selector you've used before now shares one constraint: it can only match an element based on things the browser already knows by the time it reaches that element — the element's own attributes and state, its ancestors, or the siblings that came before it in the document. A selector has never been able to reach forward into an element's descendants, or sideways into siblings that come after it, because that would mean the browser has to look ahead before deciding how to style something earlier on the page.
:has() removes that constraint. It's a pseudo-class that matches an element if a selector passed inside its parentheses matches something relative to it — a descendant, a following sibling, or a combination. People call it the "parent selector" because the most common use is exactly that: .card:has(img) matches .card elements that contain an img somewhere inside them, letting you style the card based on what's inside it, something CSS genuinely could not do before.
This isn't sugar for an existing capability — it's a new one. Before :has(), "style this element differently depending on what it contains, or what its sibling is doing" required JavaScript: attach a listener, check the condition, toggle a class. :has() lets the browser evaluate that condition natively, as part of the cascade.
/* Only cards that actually contain an image get the image treatment */
.card:has(img) {
grid-template-rows: auto 1fr;
}
.card:not(:has(img)) {
grid-template-rows: 1fr;
}
/* A form label styled by its sibling input's validity — no JS listener */
label:has(+ input:invalid) {
color: var(--color-wrong);
}
Two identical .card containers, styled the same way so far — neither has been evaluated yet.
The first rule pair is the "parent selector" case: two visually identical .card elements get different layouts purely because one of them happens to contain an img and the other doesn't — no class was added by JavaScript to signal that, CSS worked it out by inspecting the card's own descendants. The second rule is the sibling case: label:has(+ input:invalid) reads as "a label immediately followed by an invalid input," which lets a label turn red the instant its paired input fails validation, without a change or input event listener anywhere.
:has() is Baseline / widely supported now across modern browsers — it's no longer the "wait and see" feature it was when it first shipped, but if a product still supports older browser versions, pair it with a plain fallback rule and treat the :has() rule as progressive enhancement, not the only styling path..card:has(img) selects the .card, never the img — a common misreading is expecting it to select the descendant, the way a normal descendant combinator would.:has() is more expensive than a simple selector — browsers have shipped real optimizations for it, but scoping it to a specific ancestor (.card:has(img) rather than body:has(img)) keeps the evaluation cheap.document.querySelector plus a manually toggled class for "does this container have an error inside it" is exactly the pattern :has() was built to eliminate, but it's not a general replacement for JavaScript state management."How would you style a form field's label differently when its input is invalid, without JavaScript?" is a real question that tests whether a candidate knows :has() exists and reaches for it before reaching for a change listener and a manually toggled class.