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.