On this page
Modern CSS Selectors: :is(), :where(), :has()
Why Modern CSS Selectors Matter
For years, CSS developers wished for a CSS parent selector and a shorter way to write long selector lists. Today :is(), :where() and :has() deliver exactly that. These modern CSS selectors reduce repetition, lower specificity problems and remove the need for JavaScript in many UI states. Our focus keyword for this lesson is modern CSS selectors, and you will also learn trending topics such as the CSS :has() parent selector, CSS :is() vs :where(), forgiving selector lists, CSS nesting with :is(), and container-friendly component styling.
The :is() Pseudo-class
:is() takes a list of selectors and matches any element that matches at least one of them. It shortens repetitive code.
/* Before */
header a:hover, nav a:hover, footer a:hover { color: tomato; }
/* After */
:is(header, nav, footer) a:hover { color: tomato; }Key facts about :is()
- Its selector list is forgiving: if one selector inside is invalid or unsupported, the others still work. In older syntax, one invalid selector would invalidate the entire rule.
- The specificity of
:is()equals the specificity of its most specific argument. So:is(#id, .class)counts as an ID selector, even when the matched element used the class. - It cannot target pseudo-elements such as
::beforeinside the list.
The :where() Pseudo-class
:where() works exactly like :is() with one massive difference: its specificity is always zero. That makes it perfect for base styles, resets, and library defaults that authors should be able to override easily.
:where(ul, ol) { padding-inline-start: 1.5rem; }
/* Any simple selector overrides it */
.menu { padding-inline-start: 0; }:is() vs :where() at a glance
| :is() | :where() | |
|---|---|---|
| Matches selector list | Yes | Yes |
| Forgiving list | Yes | Yes |
| Specificity | Highest argument | Always 0 |
| Best for | Shortening component rules | Resets, defaults, low-priority styles |
The :has() Relational Pseudo-class (Parent Selector)
:has() selects an element based on what it contains or what follows it. It is called the CSS parent selector because for the first time you can style a parent depending on its children.
/* Card that contains an image */
.card:has(img) { display: grid; grid-template-columns: 120px 1fr; }
/* Label of an invalid input */
label:has(input:invalid) { color: crimson; }
/* Heading immediately followed by a paragraph */
h2:has(+ p) { margin-block-end: .25rem; }Powerful :has() patterns
- Form validation styling without JavaScript:
.field:has(input:user-invalid). - Theme toggles:
html:has(#dark-toggle:checked)can switch color variables for the whole page. - Conditional layouts: change a card layout if it contains a figure, a video or a button.
- Quantity queries:
ul:has(li:nth-child(5))styles a list only when it has at least five items. - Sibling lookahead:
.step:has(+ .step.active)styles the step before the active one.
Rules and limitations
:has()cannot be nested inside another:has().- Pseudo-elements are not valid inside
:has(). - Its specificity equals its most specific argument, just like
:is(). - The list is not forgiving in the same way as
:is(). An invalid selector inside can invalidate the rule in some browsers, so test carefully. - Browser support:
:has()became supported in all major evergreen browsers (Chrome, Safari, Edge, Firefox) by the end of 2023, so it is safe for most modern projects. Use@supports selector(:has(*))if you need a fallback.
Bonus: :not() for Exclusion
:not() completes the toolkit. Modern browsers accept a selector list inside it: a:not(.button, .nav-link) { text-decoration: underline; }. Like :is(), its specificity is that of its most specific argument.
Combining with CSS Nesting
Native CSS nesting works well with these selectors. Notice how the nested rule uses & together with :is() and :has():
.card {
padding: 1rem;
&:has(img) { display: grid; }
:is(h2, h3) { margin: 0; }
}Performance Notes
Browsers have optimized :has() heavily, but be sensible. Avoid extremely broad anchors like *:has(.x) or body:has(...) chains with deep descendant checks in huge DOMs. Prefer scoping to a component class, for example .card:has(img).
Best Practices
- Use
:where()for library defaults so that users can override without specificity wars. - Use
:is()to group parents and reduce repetition, but remember the specificity bump. - Scope
:has()to a component class for clarity and speed. - Provide fallbacks with
@supports selector(:has(*))when supporting older browsers. - Keep selectors readable. If a rule needs three nested functional pseudo-classes, refactor with a class.
Summary
:is() shortens selector lists, :where() does the same with zero specificity, and :has() unlocks the long-awaited parent selector. Together they let you write shorter, more powerful, and more maintainable CSS. Experiment with the live examples below.
Press Run to execute.
Press Run to execute.
Press Run to execute.
Press Run to execute.
Press Run to execute.
The CSS below repeats the same declaration for three selectors. Rewrite it using a single :is() selector so the result is identical. Do not change the HTML.
Press Run to execute.
Show expected output
All h2 and h3 elements inside section, article and aside are teal, using one rule.This is a self-check — compare your result with the expected output above.
Using :has(), give every .post that contains a <video> element a dark background (#111) and white text. Posts without a video must stay unchanged.
Press Run to execute.
Show expected output
Only the second post has a dark background and white text.This is a self-check — compare your result with the expected output above.
Was this page helpful?