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.
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.
What You'll Learn
💡 Think of It Like This
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.
Understanding Advanced CSS Selectors
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.
Selector Comparison
Selector
Purpose
Specificity
Browser Support
:has(sel)
Style parent based on child
Same as argument
Dec 2023+
:is(a, b)
Group selectors
Highest in list
2020+
:where(a, b)
Group selectors (overridable)
Always 0
:not(sel)
Exclude matching elements
All browsers
:nth-child(An+B)
Pattern-based position
0-1-0
:has() — The Parent Selector
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.
:is() vs :where() — Grouping with Different Specificity
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() and :nth-child() — Exclusion & Patterns
: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.
Formula
Selects
Example
2n
Every even item
2, 4, 6, 8...
2n+1
Every odd item
1, 3, 5, 7...
3n
Every 3rd item
3, 6, 9, 12...
-n+3
First 3 items only
1, 2, 3
n+4
4th item and beyond
4, 5, 6, 7...
Combining Selectors — Real-World Patterns
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."
When to Use These Selectors
- :has() — Form validation styling, cards that adapt based on content, conditional layouts (e.g., sidebar present vs. absent).
- :is() — Grouping heading styles, navigation links, any place you'd repeat the same parent context.
- :where() — CSS resets, base utility layers, library defaults that users should easily override.
- :not() — Styling all items except disabled/active ones, excluding the last item from a border/margin pattern.
- :nth-child() — Zebra-striped tables, highlighting first N items, grid patterns, staggered animation delays.
⚠️ Common Mistakes
- Confusing :is() and :where() specificity — :is(#id) has the specificity of an ID selector; :where(#id) has zero. This matters when rules conflict.
- Forgetting :has() is "live" — It re-evaluates as the DOM changes, which can cause layout shifts if used carelessly on dynamically-loaded content.
- Using :not() with multiple arguments in old browsers — :not(.a, .b) works in modern CSS but fails in older browsers. Use :not(.a):not(.b) for broader support.
- Nesting :has() inside :has() — Browsers explicitly forbid :has(:has(...)) . Keep :has() at a single level.
- Forgetting that :nth-child() counts all siblings — If your list mixes element types, use :nth-of-type() instead to count only matching tags.
- Over-relying on :has() for layout — While :has() is powerful, using it for critical layout decisions can make your CSS harder to debug. Use it for progressive enhancement, not structural layout.
🎉 Lesson Complete
- ✅ :has() styles parents based on children — the long-awaited parent selector
- ✅ :is() groups selectors and keeps the highest specificity from the list
- ✅ :where() groups selectors with zero specificity — perfect for overridable defaults
- ✅ :not() excludes elements — combine with other pseudo-classes for precision
- ✅ :nth-child(An+B) enables pattern-based selection like every 3rd or first 5 items
- ✅ Combining selectors unlocks powerful patterns like CSS-only form validation
- ✅ Use :where() for base styles and :is() when specificity matters
- ✅ Always test :has() with dynamic content to avoid unexpected layout shifts
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.
Continue this course
- Previous: Cascade Layers
- Next: Design Systems