Advanced Selectors Deep
Modern CSS pseudo-classes let you match elements by context and condense selector lists: :has() is the long-awaited "parent" selector, :is() groups selectors while keeping their specificity, and :where() does the same but always with zero specificity.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free HTML & CSS course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Think of CSS selectors like smart filters on a photo app. Basic selectors are like picking "all portraits." :has() is like saying "show me all albums that contain a sunset photo." :is() bundles multiple filters into one button. :where() does the same but lets any future filter override it easily — like a suggestion rather than a rule.
For most of CSS history, selectors could only look down the DOM tree — you could style a child based on its parent, but never the other way around. The introduction of :has() in 2023 changed everything. Now you can style a parent based on what it contains , unlocking patterns that previously required JavaScript.
Meanwhile, :is() and :where() solve a different problem: selector repetition . Instead of writing the same rule for article h1 , article h2 , and article h3 , you group them into a single, readable selector. The difference between :is() and :where() is subtle but critical — it's all about specificity , which determines which rule wins when multiple rules conflict.
Finally, :not() and :nth-child() round out the toolkit. Together, these five pseudo-classes let you express almost any selection logic in pure CSS, reducing your reliance on extra classes and JavaScript DOM manipulation.
For years, CSS couldn't style a parent based on its children. :has() finally solves this. It selects elements that contain a matching descendant.
The first rule says: "Any .card that contains an <img> should get a blue border." The second highlights a form when any input inside it is invalid — something that previously required JavaScript event listeners.
Both :is() and :where() group selectors to reduce repetition, but they differ in one critical way — specificity :
When to use which? Use :is() when you want your grouped rule to have normal specificity. Use :where() when building base/utility styles that should be easily overridable by any more specific rule — it's perfect for CSS resets and default styles.
:not() excludes elements from a selection. It's incredibly useful for styling "everything except" a specific class or state. Meanwhile, :nth-child(An+B) lets you select elements by their position using a formula.
The real power of advanced selectors emerges when you combine them. For example, .form-group:has(:invalid:not(:placeholder-shown)) highlights a form field container only when the user has typed invalid input — not when the field is empty. This creates a polished validation UX with zero JavaScript.
Similarly, combining :is() with :not() lets you target active/inactive states cleanly, and mixing :not() with :nth-child() enables patterns like "style every odd incomplete task differently."
Practice quiz
What does the :has() pseudo-class let you do that wasn't possible before?
- Animate any property
- Style a parent based on what it contains
- Select elements by their text content
- Override specificity entirely
Answer: Style a parent based on what it contains. :has() is the long-awaited parent selector — it matches an element that contains a descendant matching its argument.
What does .card:has(img) select?
- Every img inside a .card
- Any .card that contains an img
- The first img on the page
- Cards that are images
Answer: Any .card that contains an img. :has() styles the parent: any .card containing an img matches and gets the rule.
How does :is() affect specificity?
- It always contributes zero specificity
- It takes the specificity of the most specific selector in its list
- It doubles the specificity
- It removes specificity from neighbouring selectors
Answer: It takes the specificity of the most specific selector in its list. :is() groups selectors and takes the highest specificity from its argument list.
What is the specificity of :where()?
- The highest in its list
- The lowest in its list
- Always zero
- The same as an ID selector
Answer: Always zero. :where() always contributes zero specificity, making its rules easy to override — ideal for resets and base styles.
When should you reach for :where() over :is()?
- When you want the rule to be hard to override
- When building base/utility styles that should be easily overridable
- When targeting IDs only
- When you need higher specificity
Answer: When building base/utility styles that should be easily overridable. Use :where() for overridable defaults (resets, base styles); use :is() when you want normal specificity.
What does the :not() pseudo-class do?
- Selects the negation — elements that do NOT match its argument
- Disables an element
- Inverts the colours
- Hides an element
Answer: Selects the negation — elements that do NOT match its argument. :not(sel) excludes elements matching sel, letting you style 'everything except' a class or state.
Which :nth-child() formula selects only the first three items?
- 3n
- 2n+1
- -n+3
- n+3
Answer: -n+3. -n+3 matches positions 1, 2 and 3 — the first three items.
Which :nth-child() formula selects every third item (3, 6, 9, ...)?
- 3n
- n+3
- -n+3
- 3n+1
Answer: 3n. 3n matches every third element: 3, 6, 9, 12, and so on.
Browsers explicitly forbid which of these :has() usages?
- .card:has(img)
- Nesting :has() inside :has(), e.g. :has(:has(...))
- .form:has(:invalid)
- :has() combined with :not()
Answer: Nesting :has() inside :has(), e.g. :has(:has(...)). :has() may not be nested inside another :has(). Keep it at a single level.
If a list mixes element types and you want to count only one tag, which should you use instead of :nth-child()?
- :nth-of-type()
- :first-child
- :only-child
- :nth-last-child()
Answer: :nth-of-type(). :nth-child() counts all siblings; :nth-of-type() counts only siblings of the matching element type.