Article Heading
The :is() selector applies these styles to headings and paragraphs inside article, section, or aside elements.
Modern CSS provides powerful selectors that simplify complex styling patterns and give you precise control over specificity. The :has(), :is(), :where(), and :not() selectors enable you to write more maintainable CSS by reducing repetition, creating parent-based logic, and managing specificity conflicts. Understanding these selectors and their specificity impact is essential for building scalable CSS systems.
The :has() selector is often called the "parent selector" because it allows you to style an element based on its descendants or siblings. This reverses the normal flow of CSS, where you select children based on parents. With :has(), you can select parents based on their children.
The :has() selector takes another selector as an argument and matches elements that contain descendants matching that selector.
/* Select articles that contain an image */
article:has(img) {
display: grid;
grid-template-columns: 1fr 1fr;
}
/* Select cards that have a .premium badge */
.card:has(.badge.premium) {
border: 2px solid gold;
background: linear-gradient(to bottom, #fff9e6, white);
}
/* Select forms that contain invalid inputs */
form:has(input:invalid) {
border-left: 3px solid red;
}
These patterns were previously impossible or required JavaScript. Now you can create conditional styling based on content presence entirely in CSS.
The :has() selector works with sibling combinators, enabling you to style elements based on their siblings.
/* Style a heading differently if followed by an image */
h2:has(+ img) {
margin-bottom: 0.5rem;
}
/* Style list items that are followed by a nested list */
li:has(> ul) {
font-weight: 600;
}
/* Select sections that don't have a following section */
section:not(:has(+ section)) {
margin-bottom: 4rem;
}
Toggle the image checkbox to see how :has() changes the card layout based on content presence.
When this card contains an image, the :has() selector triggers a new (different) grid layout with colored border and background. Without an image, it uses the default flow layout. The CSS watches for img presence and applies styles automatically.
The :is() selector reduces repetition by grouping selectors together. Instead of writing multiple similar selectors, you can combine them into one rule. This makes CSS more maintainable and easier to update.
Before :is(), you needed to repeat selectors when styling multiple elements within different contexts. The :is() selector eliminates this repetition.
/* Old approach: repetitive */
article h1,
article h2,
article h3 {
font-family: Georgia, serif;
}
section h1,
section h2,
section h3 {
font-family: Georgia, serif;
}
aside h1,
aside h2,
aside h3 {
font-family: Georgia, serif;
}
/* Modern approach: using :is() */
:is(article, section, aside) :is(h1, h2, h3) {
font-family: Georgia, serif;
}
The :is() version is more concise and easier to maintain. If you need to add h4 to the list, you only change it in one place.
The :is() selector truly shines when working with complex selector patterns that would otherwise require extensive repetition.
/* Style links in navigation or footer */
:is(nav, footer) a {
text-decoration: none;
color: var(--color-primary);
}
/* Style hover states for multiple button types */
:is(.btn-primary, .btn-secondary, .btn-outline):hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgb(0 0 0 / 0.15);
}
/* Target headings in specific contexts */
:is(.hero, .feature, .testimonial) :is(h1, h2) {
text-align: center;
max-width: 40ch;
margin-inline: auto;
}
See how :is() applies the same styles to multiple element types at once. Toggle elements to see consistent styling across different contexts.
The :is() selector applies these styles to headings and paragraphs inside article, section, or aside elements.
Same styling rules apply here because :is() matches all three container types with one selector.
The :where() selector works exactly like :is() with one critical difference: it has zero specificity. This makes :where() perfect for default styles that should be easy to override.
When you use :where(), the selector inside contributes no specificity to the rule. This allows any later selector, even a simple class, to override the styles without specificity conflicts.
/* Using :is() - takes highest specificity from list */
:is(#sidebar, .widget, article) p {
color: navy;
}
/* Specificity: 1,0,1 (from #sidebar) */
/* Using :where() - zero specificity */
:where(#sidebar, .widget, article) p {
color: navy;
}
/* Specificity: 0,0,1 (only from p) */
/* This override works with :where() but not :is() */
.custom-color {
color: red;
}
/* Specificity: 0,1,0 */
With :where(), the .custom-color class successfully overrides the paragraph color because 0,1,0 beats 0,0,1. With :is(), the ID inside the selector creates 1,0,1 specificity, which requires an ID or !important to override.
Use :where() for base styles in design systems or CSS resets. This ensures component-specific styles can easily override defaults without specificity battles.
/* Base button styles using :where() for easy overrides */
:where(button, .btn) {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.375rem;
font-size: 1rem;
cursor: pointer;
background: oklch(0.90 0.02 250);
color: oklch(0.20 0.02 250);
}
/* Component styles easily override without specificity issues */
.btn-primary {
background: var(--color-primary);
color: white;
}
.btn-danger {
background: var(--color-danger);
color: white;
}
Compare how :where() and :is() handle specificity differently. Try overriding the base color with the class selector.
This paragraph's color is set by :is(#demo-is, .demo-box). The ID gives it high specificity, so the .override-color class cannot override it.
This paragraph's color is set by :where(#demo-where, .demo-box). Because :where() has zero specificity, the .override-color class easily overrides it.
The :not() selector excludes elements from selection. Modern CSS enhances :not() to accept multiple selectors, making it more powerful for complex exclusions.
/* Style all paragraphs except those with .intro class */
p:not(.intro) {
color: oklch(0.30 0.02 250);
}
/* Style links that are not in navigation */
a:not(nav a) {
text-decoration: underline;
}
/* Style list items except the first one */
li:not(:first-child) {
margin-top: 0.5rem;
}
Modern :not() accepts multiple selectors separated by commas, excluding any element that matches any of the selectors.
/* Exclude multiple classes */
button:not(.primary, .secondary, .outline) {
background: var(--color-neutral);
}
/* Style inputs except buttons and submits */
input:not([type="button"], [type="submit"], [type="reset"]) {
border: 1px solid oklch(0.80 0.02 250);
padding: 0.5rem;
}
/* Style sections except hero and footer */
section:not(.hero, .footer) {
padding: 2rem;
max-width: 1200px;
margin-inline: auto;
}
See how :not() excludes specific elements from styling. Toggle exclusions to see which items receive the styles.
The CSS rule li:not(.excluded-first, .excluded-second) applies colored background and border only to items without those classes.
Understanding how these modern selectors affect specificity helps you write predictable CSS and avoid specificity conflicts.
/* :is() takes highest specificity */
:is(#id, .class, div) { }
/* Specificity: 1,0,0 (from #id) */
/* :where() always zero */
:where(#id, .class, div) { }
/* Specificity: 0,0,0 */
/* :not() uses argument specificity */
div:not(.excluded) { }
/* Specificity: 0,1,1 (div + .excluded) */
/* :has() uses argument specificity */
article:has(> img) { }
/* Specificity: 0,0,2 (article + img) */
/* Combined selectors */
:is(nav, aside) :where(.link, a) { }
/* Specificity: 0,0,1 (nav/aside + 0 from :where) */
When working with complex selectors, specificity calculators help you understand why certain styles apply or do not apply. Here are reliable tools:
Seeing these selectors in action through before and after comparisons demonstrates their value in real codebases.
/* Before: repetitive selector lists */
.article h1,
.article h2,
.article h3,
.sidebar h1,
.sidebar h2,
.sidebar h3,
.footer h1,
.footer h2,
.footer h3 {
font-weight: 700;
line-height: 1.2;
}
/* After: using :is() */
:is(.article, .sidebar, .footer) :is(h1, h2, h3) {
font-weight: 700;
line-height: 1.2;
}
/* Before: multiple selectors for each state */
input:focus,
textarea:focus,
select:focus {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
input:invalid,
textarea:invalid,
select:invalid {
border-color: var(--color-danger);
}
/* After: using :is() */
:is(input, textarea, select):focus {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
:is(input, textarea, select):invalid {
border-color: var(--color-danger);
}
/* Before: high specificity makes overrides difficult */
nav ul li a {
color: navy;
text-decoration: none;
}
/* Specificity: 0,0,4 - requires complex selector to override */
/* After: using :where() for easy overrides */
:where(nav ul li) a {
color: navy;
text-decoration: none;
}
/* Specificity: 0,0,1 - any class can override */
/* Now this works easily */
.special-link {
color: red;
}
/* Before: required multiple classes and JavaScript */
.card.has-image {
display: grid;
grid-template-columns: 200px 1fr;
}
/* Needed JS: card.classList.add('has-image') */
/* After: pure CSS with :has() */
.card:has(img) {
display: grid;
grid-template-columns: 200px 1fr;
}
/* No JavaScript needed */
Modern selectors have excellent browser support as of 2024. All major browsers support :is(), :where(), and :not(). The :has() selector achieved baseline support in 2023 and is now widely available.
Check current browser compatibility at Can I Use. Search for specific selectors like ":has selector" or ":where selector" to see detailed support information and usage statistics.
For production use, these selectors are safe to use without fallbacks in modern web applications. If you need to support older browsers, consider them as progressive enhancements where the page remains functional without them.
:where() to ensure easy overrides
:has() is more performant than adding classes with JavaScript
:where() for specificity control, add comments explaining why
/**
* Analyze the following selectors and answer:
* 1. What is the specificity of each selector?
* 2. Which selector would win if both targeted the same element?
* 3. How could you refactor these using :is() or :where()?
* 4. When would you choose :where() over :is()?
*/
/* Selector A */
nav ul li a {
color: blue;
}
/* Selector B */
:is(nav, aside) :is(ul, ol) li a {
color: green;
}
/* Selector C */
:where(nav, aside) ul li a {
color: red;
}
/* Selector D */
.navigation a {
color: purple;
}
These modern selectors are fundamental tools you will use throughout the course. In the next assignment, you will refactor existing CSS using these selectors to reduce repetition and manage specificity conflicts. Understanding when to use each selector and how they affect specificity is essential for writing maintainable, scalable CSS systems. As you continue building more complex interfaces, these patterns will help you keep your CSS organized and predictable.