Advanced Selectors: :has(), :is(), :where()
Lesson 25 โข Advanced Track
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 | 2020+ |
| :not(sel) | Exclude matching elements | Same as argument | All browsers |
| :nth-child(An+B) | Pattern-based position | 0-1-0 | All browsers |
: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.
Try :has() Selector
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
.card { border: 2px solid #ccc; padding: 16px; margin: 12px 0; border-radius: 8px; }
.card:has(img) {
border-color: #2196F3;
background: #E3F2FD;
}
.card:has(.urgent) {
border-color: #F44336;
background: #FFEBEE;
}
.card img { width: 100%; max-width: 200px; border-radius: 4px; }
.urgent {
...: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.
Try :is() vs :where()
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
:is(h1, h2, h3) {
color: #1565C0;
border-bottom: 2px solid #BBDEFB;
padding-bottom: 4px;
}
:where(article, section, aside) p {
line-height: 1.8;
color: #555;
}
.highlight p {
color: #E65100;
font-weight: bold;
}
.demo-box {
border: 1px solid
...: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... |
Try :not() and :nth-child() Formulas
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; padding: 20px; }
ul { list-style: none; padding: 0; max-width: 400px; }
li {
padding: 10px 16px;
margin: 4px 0;
border-radius: 6px;
background: #f5f5f5;
}
li:not(.disabled) {
cursor: pointer;
background: #E8F5E9;
}
li:not(.disabled):hover {
background: #C8E6C9;
}
...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."
Combining Selectors: Validation, Tabs, & Mixed Patterns
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: system-ui, sans-serif; padding: 20px; }
/* Form validation with :has() + :invalid */
.form-group {
margin: 12px 0;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
transition: border-color 0.3s, background 0.3s;
}
.form-group:has(:invalid:not(:placeholder-shown)) {
border-color: #F44336;
background:
...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
Sign up for free to track which lessons you've completed and get learning reminders.