CSS3 & Responsive Design

Modern Selectors: :is(), :where(), :has(), :not()

30 min Lesson 52 of 60

The Evolution of CSS Selectors

CSS selectors have undergone a remarkable evolution. Early CSS gave us basic selectors like element, class, ID, and descendant selectors. Over time, we gained attribute selectors, pseudo-classes like :hover and :first-child, and combinators like the adjacent sibling selector. But writing complex selector patterns still required verbose, repetitive code. If you wanted to style headings inside articles, sections, and asides, you had to write each combination separately, creating long selector lists that were hard to maintain.

Modern CSS introduces four powerful pseudo-class selectors that fundamentally change how we write selector logic: :not() for excluding elements, :is() for matching any of several selectors with normal specificity, :where() for matching with zero specificity, and :has() for selecting parents based on their children. These selectors reduce repetition, improve readability, give you precise control over specificity, and -- in the case of :has() -- unlock an entirely new dimension of styling that was previously impossible in CSS. After this lesson, you will write cleaner, more maintainable stylesheets with significantly fewer lines of code.

The :not() Pseudo-Class

The :not() pseudo-class selects elements that do not match the given selector argument. It has been available since CSS3, but its capabilities were originally limited to accepting only a single simple selector. Modern CSS significantly expands :not() to accept complex selectors and multiple comma-separated arguments, making it vastly more powerful. Think of :not() as a filter that removes matching elements from the selection -- everything that passes the filter receives the styles.

Basic :not() Usage

At its simplest, :not() takes a single selector and excludes matching elements. This is useful for applying styles to all elements of a type except those with a specific class, attribute, or state.

Basic :not() Selectors

/* Style all paragraphs except those with class "intro" */
p:not(.intro) {
    text-indent: 1.5em;
}

/* Style all links except those being hovered */
a:not(:hover) {
    text-decoration: none;
}

/* Style all inputs except disabled ones */
input:not(:disabled) {
    border-color: var(--border-light, #ccc);
}

/* Style all list items except the last one */
li:not(:last-child) {
    margin-bottom: 0.5rem;
    border-bottom: 1px solid var(--border-light, #eee);
}

/* Style all images except those with alt text (empty alt) */
img:not([alt]) {
    outline: 3px solid red; /* Accessibility debugging */
}

/* Style all elements except the first child */
.stack > *:not(:first-child) {
    margin-top: 1rem;
}

:not() with Multiple Arguments

Modern :not() accepts a comma-separated list of selectors. The element is excluded if it matches any of the listed selectors. This is equivalent to chaining multiple :not() pseudo-classes but is much more concise and readable. For example, :not(.a, .b) is the same as :not(.a):not(.b) -- both exclude elements with class "a" or class "b".

:not() with Multiple Selectors

/* Exclude multiple classes at once */
.nav-link:not(.active, .disabled) {
    opacity: 0.7;
}

/* Style all form elements except buttons and submits */
.form-group :not(button, [type="submit"], [type="reset"]) {
    display: block;
    width: 100%;
}

/* All headings except h1 and h2 */
:is(h1, h2, h3, h4, h5, h6):not(h1, h2) {
    font-weight: 500;
}

/* Exclude multiple states */
.button:not(:disabled, .loading, [aria-busy="true"]) {
    cursor: pointer;
}

/* All children except first and last */
.list > *:not(:first-child, :last-child) {
    border-top: 1px solid var(--border-light, #eee);
    border-bottom: 1px solid var(--border-light, #eee);
}

:not() with Complex Selectors

Modern browsers allow :not() to accept complex selectors, including selectors with combinators. This means you can exclude elements based on their context -- their parent, their position in a hierarchy, or their relationship to other elements.

Complex Selectors Inside :not()

/* Paragraphs NOT inside an article */
p:not(article p) {
    max-width: 65ch;
}

/* Links NOT inside the navigation */
a:not(nav a) {
    color: var(--primary, #0066cc);
    text-decoration: underline;
}

/* Inputs NOT inside a .form-group */
input:not(.form-group input) {
    margin-bottom: 1rem;
}

/* Images NOT inside a figure element */
img:not(figure img) {
    border-radius: 0.5rem;
}

/* Divs that are NOT direct children of main */
div:not(main > div) {
    padding: 0;
}
Note: The specificity of :not() is determined by its most specific argument. :not(.foo) has the specificity of a single class selector (0, 1, 0). :not(#bar) has the specificity of an ID selector (1, 0, 0). When using multiple arguments like :not(.a, #b), the specificity is that of the most specific argument -- in this case, the ID selector (1, 0, 0). The :not() pseudo-class itself does not add to the specificity.

The :is() Pseudo-Class

The :is() pseudo-class takes a comma-separated list of selectors and matches any element that matches at least one of the selectors in the list. Its primary purpose is to reduce repetition in your stylesheets. Before :is(), styling the same property on elements in different contexts required writing out each combination individually, often resulting in long, unwieldy selector lists. With :is(), you can group the varying parts of the selector into a single, readable expression.

Reducing Selector Repetition

Consider a common scenario: you want to style all headings inside articles, sections, and asides. Without :is(), you would write a separate selector for each combination. With :is(), you express the grouping in-line, making the intent immediately clear.

Before and After :is()

/* WITHOUT :is() -- verbose and repetitive */
article h2,
article h3,
article h4,
section h2,
section h3,
section h4,
aside h2,
aside h3,
aside h4 {
    color: var(--text-dark, #1a1a1a);
    line-height: 1.3;
}

/* WITH :is() -- clean and readable */
:is(article, section, aside) :is(h2, h3, h4) {
    color: var(--text-dark, #1a1a1a);
    line-height: 1.3;
}

/* Another example: styling links in multiple containers */
/* WITHOUT :is() */
.header a:hover,
.footer a:hover,
.sidebar a:hover {
    text-decoration: underline;
}

/* WITH :is() */
:is(.header, .footer, .sidebar) a:hover {
    text-decoration: underline;
}

Notice how nine selectors collapsed into a single line. The :is() pseudo-class acts as a grouping mechanism at any position in the selector. You can use it at the beginning, in the middle, or at the end of a selector chain. Each :is() expands to all its arguments when the browser matches elements, so :is(article, section) :is(h2, h3) is equivalent to four separate selectors.

Using :is() in Different Positions

The :is() pseudo-class is not limited to the start of a selector. You can place it anywhere a simple selector would go -- at the end to group target elements, in the middle to group ancestors, or even multiple times in the same selector.

:is() in Various Positions

/* At the start: grouping parent contexts */
:is(.card, .panel, .modal) .title {
    font-size: 1.25rem;
    font-weight: 600;
}

/* At the end: grouping target elements */
.form-group :is(input, select, textarea) {
    border: 1px solid var(--border-light, #ccc);
    border-radius: 0.375rem;
    padding: 0.5rem 0.75rem;
}

/* In the middle: grouping intermediate elements */
main :is(article, section) p {
    line-height: 1.7;
    max-width: 65ch;
}

/* Multiple :is() in one selector */
:is(header, footer) :is(nav, .nav) :is(a, button) {
    display: inline-flex;
    align-items: center;
    padding: 0.5rem 1rem;
}

/* Grouping pseudo-classes */
.button:is(:hover, :focus-visible) {
    background-color: var(--primary-light, #0077ee);
    outline: none;
}

/* Grouping attribute selectors */
input:is([type="text"], [type="email"], [type="password"], [type="url"]) {
    font-family: inherit;
    font-size: 1rem;
}

:is() and Specificity

A critical detail about :is() is its specificity behavior. The specificity of :is() is equal to the most specific selector in its argument list. This means if you mix a class selector and an ID selector inside :is(), the entire :is() expression takes on the specificity of the ID selector. This is important because it can cause unexpected specificity bumps if you are not careful.

:is() Specificity Behavior

/* Specificity: (0, 1, 0) -- highest argument is .intro (class) */
:is(.intro, .summary) p {
    font-size: 1.125rem;
}

/* Specificity: (1, 0, 0) -- highest argument is #hero (ID) */
:is(#hero, .banner) p {
    font-size: 1.5rem;
}
/* WARNING: even .banner p gets ID-level specificity here! */

/* This can create problems: */
:is(#main, .content) a { color: blue; }    /* (1, 0, 1) */
.content a { color: red; }                  /* (0, 1, 1) */
/* The first rule wins for .content a because #main bumped specificity */

/* Safe pattern: keep specificity levels consistent inside :is() */
:is(.card, .panel, .modal) .title { /* all class-level: (0, 2, 0) */
    color: var(--text-dark, #1a1a1a);
}

/* Risky pattern: mixed specificity levels */
:is(#sidebar, .aside, footer) a { /* ID-level: (1, 0, 1) */
    color: inherit;
}
Important: Be careful when mixing ID selectors with class or element selectors inside :is(). The entire :is() expression inherits the specificity of the highest-specificity argument. This means :is(#hero, .banner) gives the entire match ID-level specificity, even for elements matched by .banner alone. If you need zero-specificity grouping, use :where() instead, which we cover next.

The :where() Pseudo-Class

The :where() pseudo-class is functionally identical to :is() -- it accepts a comma-separated list of selectors and matches any element that matches at least one of them. The one critical difference is specificity: :where() always has zero specificity, regardless of the selectors inside it. This makes :where() perfect for writing base styles, reset stylesheets, and default styles that you intend to be easily overridden.

Zero Specificity in Practice

Because :where() contributes zero specificity, any other selector with even minimal specificity will override it. This is a feature, not a limitation -- it means styles defined with :where() serve as defaults that any more specific rule can override without needing to escalate specificity.

:where() Zero Specificity

/* :where() has zero specificity */
:where(.card, .panel, .modal) .title {
    color: var(--text-dark, #1a1a1a);  /* Specificity: (0, 1, 0) -- only .title counts */
}

/* Compare with :is() */
:is(.card, .panel, .modal) .title {
    color: var(--text-dark, #1a1a1a);  /* Specificity: (0, 2, 0) -- .card + .title */
}

/* :where() for default form styles -- easily overridden */
:where(input, select, textarea) {
    font-family: inherit;
    font-size: 1rem;
    line-height: 1.5;
    /* Specificity: (0, 0, 0) -- any rule overrides this */
}

/* Even a simple element selector overrides :where() */
input {
    font-size: 0.875rem;  /* This wins over the :where() rule above */
    /* Specificity: (0, 0, 1) beats (0, 0, 0) */
}

/* :where() with IDs still has zero specificity */
:where(#hero, #main, #sidebar) a {
    color: inherit;  /* Specificity: (0, 0, 1) -- only the "a" counts */
}

/* A single class easily overrides it */
.link {
    color: blue;  /* (0, 1, 0) beats (0, 0, 1) */
}

:where() for CSS Resets and Defaults

The most natural use case for :where() is writing CSS resets and default styles. Traditional CSS resets often create specificity problems because they use element selectors that subsequent styles must match or exceed. With :where(), your reset has zero specificity, so every subsequent style -- even those using simple element selectors -- can override the defaults without any specificity battles.

CSS Reset Using :where()

/* Modern CSS Reset with :where() -- zero specificity */
:where(*, *::before, *::after) {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

:where(html) {
    line-height: 1.5;
    -webkit-text-size-adjust: 100%;
}

:where(body) {
    min-height: 100vh;
}

:where(img, picture, video, canvas, svg) {
    display: block;
    max-width: 100%;
}

:where(input, button, textarea, select) {
    font: inherit;
}

:where(h1, h2, h3, h4, h5, h6) {
    line-height: 1.2;
    overflow-wrap: break-word;
}

:where(p) {
    overflow-wrap: break-word;
}

:where(a) {
    color: inherit;
    text-decoration: inherit;
}

:where(ul, ol) {
    list-style: none;
}

/* All of the above have ZERO specificity */
/* Even the simplest selector will override them */
h1 { font-size: 2.5rem; }  /* Easily overrides :where(h1) */

:is() vs :where(): When to Use Each

Both :is() and :where() provide the same grouping functionality. The choice between them comes down to whether you want the grouped selectors to contribute to specificity or not. Use :is() when you are writing component styles that should compete normally in the specificity cascade. Use :where() when you are writing base styles, defaults, or utility layers that should be easy to override.

Choosing Between :is() and :where()

/* USE :where() for: base styles, resets, defaults */
/* These should be easily overridden by component styles */
:where(article, section, aside) {
    padding: 1rem;
}

:where(.btn) {
    display: inline-flex;
    align-items: center;
    padding: 0.5rem 1rem;
    border: none;
    cursor: pointer;
}

/* USE :is() for: component styles, specific patterns */
/* These should compete normally in the cascade */
:is(.card, .panel) .header {
    font-weight: 600;
    border-bottom: 1px solid var(--border-light, #eee);
}

:is(.nav-primary, .nav-secondary) a:is(:hover, :focus-visible) {
    color: var(--primary, #0066cc);
}

/* Pattern: :where() for the base, :is() for the specifics */
/* Base button (easily overridden) */
:where(.btn) {
    background: var(--bg-light, #f5f5f5);
    color: var(--text-dark, #333);
}

/* Specific variant (normal specificity, overrides base) */
.btn:is(.btn-primary) {
    background: var(--primary, #0066cc);
    color: white;
}
Pro Tip: A powerful pattern for design systems is to use :where() for all default component styles and :is() for variant and state styles. This way, your defaults are always overridable, but your specific variants and states carry enough specificity to override the defaults reliably. For example: :where(.btn) { ... } for the base button and .btn:is(.primary, .secondary) { ... } for variants.

The :has() Pseudo-Class

The :has() pseudo-class is arguably the most revolutionary addition to CSS selectors in years. It allows you to select an element based on what it contains -- its children, descendants, or even its subsequent siblings. Before :has(), CSS could only select elements based on their ancestors and preceding siblings. There was no way to style a parent based on its children, which is why developers frequently called the "parent selector" the most requested CSS feature. The :has() pseudo-class finally delivers that capability and much more.

Basic :has() as a Parent Selector

The most common use of :has() is as a parent selector: selecting a parent element based on the presence of a specific child or descendant. The syntax is parent:has(child), where the parent is selected only if it contains an element matching the child selector.

:has() as a Parent Selector

/* Select cards that contain an image */
.card:has(img) {
    display: grid;
    grid-template-rows: auto 1fr;
}

/* Cards without images get different layout */
.card:not(:has(img)) {
    padding: 2rem;
}

/* Select form groups that contain a required input */
.form-group:has(input:required) {
    position: relative;
}

/* Add asterisk to labels inside required form groups */
.form-group:has(input:required) label::after {
    content: " *";
    color: red;
}

/* Style a section differently if it contains a heading */
section:has(h2) {
    padding-top: 2rem;
    border-top: 2px solid var(--primary, #0066cc);
}

/* Select navigation that contains more than one list */
nav:has(ul + ul) {
    display: flex;
    justify-content: space-between;
}

/* Select articles that contain code blocks */
article:has(pre code) {
    font-size: 0.95rem;
}

:has() with Direct Child Combinator

You can use combinators inside :has() to be more precise about the relationship. The direct child combinator (>) ensures you are checking only immediate children, not all descendants.

:has() with Combinators

/* Select lists that have a direct child with class "active" */
ul:has(> .active) {
    background: var(--bg-light, #f8f9fa);
}

/* Select a div that directly contains an h1 */
div:has(> h1) {
    margin-bottom: 2rem;
}

/* Containers with direct child buttons */
.toolbar:has(> button) {
    display: flex;
    gap: 0.5rem;
    padding: 0.5rem;
}

/* Select fieldsets that directly contain legend + inputs */
fieldset:has(> legend):has(> input) {
    border: 2px solid var(--border-light, #ddd);
    border-radius: 0.5rem;
    padding: 1.5rem;
}

:has() for Sibling Selection

The :has() pseudo-class is not limited to parent-child relationships. You can use the adjacent sibling combinator (+) or general sibling combinator (~) inside :has() to select elements based on their subsequent siblings. This is a capability that no previous CSS selector could provide -- selecting an element based on what comes after it.

:has() with Sibling Selectors

/* Style a heading that is followed by a paragraph */
h2:has(+ p) {
    margin-bottom: 0.5rem;
}

/* Style a heading NOT followed by a paragraph */
h2:not(:has(+ p)) {
    margin-bottom: 1.5rem;
}

/* Style an image that is followed by a figcaption */
img:has(+ figcaption) {
    border-radius: 0.5rem 0.5rem 0 0;
}

/* Style a label that has a sibling input which is checked */
label:has(~ input:checked) {
    font-weight: bold;
    color: var(--primary, #0066cc);
}

/* Style an input that is followed by an error message */
input:has(+ .error-message) {
    border-color: red;
    background: #fff0f0;
}

/* Style a dt when its dd sibling contains specific content */
dt:has(+ dd img) {
    font-size: 1.25rem;
}

:has() for Form Validation Styles

One of the most practical applications of :has() is styling forms based on validation state. You can style entire form groups, labels, helper text, and surrounding containers based on whether inputs are valid, invalid, focused, or filled. This was previously impossible without JavaScript -- CSS alone could only style the input itself, not its surrounding context.

Form Validation with :has()

/* Style form group when its input is focused */
.form-group:has(input:focus, textarea:focus, select:focus) {
    background: var(--bg-light, #f8f9fa);
    border-radius: 0.5rem;
    padding: 0.75rem;
}

/* Highlight label when input is focused */
.form-group:has(input:focus) label {
    color: var(--primary, #0066cc);
    font-weight: 600;
}

/* Style form group when input is invalid and has been interacted with */
.form-group:has(input:invalid:not(:placeholder-shown)) {
    border-left: 3px solid red;
    padding-left: 1rem;
}

/* Show error text only when input is invalid */
.form-group:has(input:invalid:not(:placeholder-shown)) .error-text {
    display: block;
    color: red;
    font-size: 0.875rem;
    margin-top: 0.25rem;
}

/* Hide error text by default */
.form-group .error-text {
    display: none;
}

/* Style form group when input is valid */
.form-group:has(input:valid:not(:placeholder-shown)) {
    border-left: 3px solid green;
    padding-left: 1rem;
}

/* Style entire form when it has any invalid inputs */
form:has(input:invalid) .submit-btn {
    opacity: 0.5;
    cursor: not-allowed;
}

/* Style form when all required inputs are valid */
form:not(:has(input:required:invalid)) .submit-btn {
    opacity: 1;
    cursor: pointer;
    background: var(--primary, #0066cc);
}

/* Checkbox group: style parent when checkbox is checked */
.checkbox-wrapper:has(input[type="checkbox"]:checked) {
    background: var(--primary-light, #e6f0ff);
    border-color: var(--primary, #0066cc);
}

/* Radio group: highlight selected option */
.radio-option:has(input[type="radio"]:checked) {
    background: var(--primary-light, #e6f0ff);
    border: 2px solid var(--primary, #0066cc);
    font-weight: 600;
}
Pro Tip: The pattern :invalid:not(:placeholder-shown) is a clever way to avoid showing validation errors on empty fields before the user has started typing. The :placeholder-shown pseudo-class matches inputs whose placeholder text is currently visible (meaning the user has not entered any value). By combining it with :not(), you only apply invalid styles after the user has typed something and it is still invalid.

:has() for Conditional Layouts

The :has() selector opens up entirely new layout patterns. You can change a container's grid or flexbox configuration based on the number or type of its children. You can apply different styles to a page based on what components it contains. This is truly conditional CSS -- the layout adapts based on the content, not just the viewport size.

Conditional Layouts with :has()

/* Change grid layout based on whether sidebar exists */
.page:has(.sidebar) {
    display: grid;
    grid-template-columns: 1fr 300px;
    gap: 2rem;
}

.page:not(:has(.sidebar)) {
    max-width: 800px;
    margin-inline: auto;
}

/* Card layout changes based on content type */
.card:has(img):has(.card-body) {
    display: grid;
    grid-template-columns: 200px 1fr;
    align-items: start;
}

.card:has(img):not(:has(.card-body)) {
    text-align: center;
}

/* Adjust navigation for different amounts of items */
nav:has(li:nth-child(6)) {
    /* More than 5 items: use a different layout */
    flex-wrap: wrap;
    justify-content: center;
}

/* Empty state styling */
.list:not(:has(li)) {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 200px;
    background: var(--bg-light, #f8f9fa);
    color: var(--text-light, #666);
}

.list:not(:has(li))::before {
    content: "No items to display";
    font-style: italic;
}

/* Dark mode toggle based on a checkbox */
body:has(#dark-mode-toggle:checked) {
    --bg-light: #1a1a2e;
    --bg-white: #16213e;
    --text-dark: #e0e0e0;
    --text-light: #aaa;
    --border-light: #333;
}

Combining Modern Selectors

The true power of modern selectors emerges when you combine them. You can use :is() for grouping, :not() for exclusion, :where() for zero-specificity defaults, and :has() for parent/relational selection -- all in the same stylesheet, and sometimes in the same selector. These pseudo-classes compose naturally, letting you express complex selection logic that would have been impossible or impractical with older CSS.

Combining All Modern Selectors

/* Cards with images but not the featured card */
.card:has(img):not(.featured) {
    grid-template-columns: 120px 1fr;
}

/* Base styles with :where(), enhanced with :is() */
:where(.card) {
    padding: 1rem;
    border: 1px solid var(--border-light, #eee);
}

:is(.card):is(:hover, :focus-within) {
    border-color: var(--primary, #0066cc);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

/* Style headings inside containers that have specific content */
:is(section, article):has(.important) :is(h2, h3) {
    color: var(--primary, #0066cc);
    border-left: 3px solid currentColor;
    padding-left: 0.75rem;
}

/* Form groups that are NOT disabled AND have focused inputs */
.form-group:not(:has(:disabled)):has(:focus) {
    outline: 2px solid var(--primary, #0066cc);
    outline-offset: 4px;
    border-radius: 0.375rem;
}

/* :where() defaults that :has() can enhance */
:where(.grid) {
    display: grid;
    gap: 1rem;
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}

/* When grid has wide items, adjust */
.grid:has(.wide-item) {
    grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
}

/* Complex navigation pattern */
:is(nav, .nav):has(ul):not(:has(.mobile-menu-open)) ul {
    display: flex;
    gap: 1rem;
    list-style: none;
}

/* Accessible focus management */
:is(a, button, input, select, textarea):focus-visible:not(:has(*)) {
    outline: 2px solid var(--primary, #0066cc);
    outline-offset: 2px;
}

Building a Component System

Let us look at how modern selectors can be used to build a complete, adaptive component system. This example demonstrates a card component that adapts its layout based on its content, states, and context -- all without JavaScript or extra class names.

Adaptive Card Component

/* Base card with :where() for easy overrides */
:where(.card) {
    border: 1px solid var(--border-light, #e0e0e0);
    border-radius: 0.75rem;
    overflow: hidden;
    background: var(--bg-white, #ffffff);
}

/* Card with image: horizontal layout */
.card:has(> img, > .card-image) {
    display: grid;
    grid-template-columns: clamp(120px, 30%, 250px) 1fr;
}

/* Card without image: stack layout */
.card:not(:has(img)) {
    display: flex;
    flex-direction: column;
    padding: var(--card-padding, 1.5rem);
}

/* Card with many items: adjust spacing */
.card:has(.card-body > *:nth-child(4)) .card-body {
    gap: 0.5rem;
}

/* Card states using :is() for grouping */
.card:is(:hover, :focus-within):not(.disabled) {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    transform: translateY(-2px);
    transition: all 0.2s ease;
}

/* Card actions: change button style based on card context */
.card:has(.card-badge.urgent) :is(.card-action, .card-btn) {
    background: #dc3545;
    color: white;
}

/* Responsive behavior within grid */
.card-grid:has(.card:nth-child(4)) {
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 280px), 1fr));
}

.card-grid:not(:has(.card:nth-child(4))) {
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 350px), 1fr));
}

Browser Support

Browser support for modern selectors varies by feature. The :not() pseudo-class with a single simple selector has been supported since CSS3 and works everywhere. The enhanced :not() with multiple arguments and complex selectors is supported in all modern browsers including Chrome 88+, Firefox 84+, Safari 14+, and Edge 88+. The :is() and :where() pseudo-classes share the same support: Chrome 88+, Firefox 82+, Safari 14+, and Edge 88+. The :has() pseudo-class was the last to arrive and is supported in Chrome 105+, Firefox 121+, Safari 15.4+, and Edge 105+. As of 2025, all four selectors are safe to use in production for modern web projects.

Note: When using :has(), keep in mind that it can have performance implications if used with very broad selectors. The browser must evaluate the :has() condition whenever the DOM changes, so *:has(.something) forces the browser to check every element. For best performance, always qualify your :has() selectors with a specific element or class, like .card:has(img) rather than *:has(img) or :has(img) alone.
Common Mistake: A frequent error is assuming that :is() and :where() behave identically. While they match the same elements, their specificity is completely different. If you write default styles with :is() when you meant to use :where(), those defaults may be unexpectedly difficult to override. Conversely, if you use :where() for component styles that need to override base styles, they may be too weak. Always ask yourself: "Should this selector contribute to specificity?" If yes, use :is(). If no, use :where().

Practice Exercise

Build an adaptive form component that uses all four modern selectors. Create an HTML form with the following structure: three text inputs (name, email, message), two checkboxes (terms and newsletter), a radio group with three options (support, sales, general), and a submit button. Each input should be wrapped in a .form-group div with a label and an optional .error-text span. Now write CSS that accomplishes the following using only modern selectors -- no JavaScript: (1) Use :where() to set zero-specificity default styles for all form elements (font, padding, border, border-radius). (2) Use :is() to group hover and focus styles for all interactive elements (inputs, selects, textareas, buttons) so they share a common focus ring style. (3) Use :not() to style all .form-group elements except the last one with a bottom margin and border. Use :not(:disabled) to ensure only enabled inputs get the interactive styles. (4) Use :has() to style .form-group containers based on their input state: highlight the group when its input is focused, show a green left border when the input is valid and not empty, show a red left border when the input is invalid and not empty, and display the .error-text only when the input is in an invalid state. (5) Use :has() to style the submit button -- make it appear disabled (reduced opacity, not-allowed cursor) when the form contains any required invalid inputs, and fully operable when all required fields are valid. (6) Use :has() with checkbox and radio inputs to highlight the selected option's wrapper. (7) Combine :is(), :not(), and :has() in at least two selectors to demonstrate how they compose. Test your form by filling in fields, leaving fields empty, entering invalid emails, and checking boxes to verify all visual states update correctly without any JavaScript.