Welcome to the fourth installment of our five-part series on modern CSS. Throughout this series, we have explored how the web styling landscape has shifted from relying on heavy external tooling toward adopting powerful, standardized browser features. In earlier parts, we covered modern layout algorithms, fluid typography, and native cascade layers. Today, we turn our attention to one of the most anticipated workflow transformations in web development history: native CSS Nesting and the new generation of advanced functional selectors.
For more than a decade, front-end developers turned to preprocessors like Sass, Less, or PostCSS primarily for a single feature: nesting. Writing hierarchically structured style rules made components easier to read and maintain. However, preprocessors require build pipelines, compilation steps, and node dependencies. Today, modern browsers support native nesting directly, along with relational and specificity-management selectors such as :has(), :is(), and :where(). Whether you are following our entire series or jumping straight into this guide, you will learn how these features allow you to write clean, modular, and maintainable styles without a single compilation step.
The Arrival of Native CSS Nesting
The CSS Nesting Module specification brings the organizational syntax developers love straight to the browser engine. As of 2023, native nesting is supported across all major evergreen browsers (Chrome, Edge, Firefox, and Safari). This milestone means that for the vast majority of web projects, you can nest styles natively in production without installing Sass or running a watcher daemon.
While native nesting looks very similar to preprocessor nesting at first glance, understanding how the browser processes these rules is critical. In a preprocessor, nesting is merely “syntactic sugar” that gets flattened into standard multi-class selectors during compilation. In native CSS, nesting is parsed directly by the browser at runtime, retaining structural relationships within the CSS Object Model (CSSOM).
Understanding the Syntax: The Ampersand and Relaxed Parsing
When native nesting was first drafted, the specification strictly required that every nested rule start with a symbol—most commonly the ampersand (&)—so browser parsers could easily distinguish between an element selector and a CSS property. However, thanks to the updated “relaxed parsing” specification now implemented across modern engines, you can nest selectors almost identically to how you would in Sass.
The ampersand symbol represents the parent selector. It allows you to define pseudo-classes, pseudo-elements, compound selectors, and modifiers directly within the parent block.
.button {
background-color: #0066cc;
color: #ffffff;
padding: 0.75rem 1.5rem;
border-radius: 4px;
/* Targeting pseudo-classes on the parent */
&:hover {
background-color: #0052a3;
}
&:focus-visible {
outline: 2px solid #ffbf00;
}
/* Targeting descendant elements */
.icon {
display: inline-block;
margin-right: 0.5rem;
}
}
With relaxed parsing, you can even omit the ampersand when targeting descendant elements. For example, writing span { font-weight: bold; } inside .button { ... } is completely valid in modern browsers. However, using the ampersand explicitly remains a recommended practice among many development teams because it instantly communicates structural intent and avoids visual ambiguity between properties and selectors.
A Crucial Difference from Sass: String Concatenation
One major pitfall that catches developers transitioning from preprocessors is selector concatenation. In Sass, developers frequently use the ampersand to assemble BEM (Block Element Modifier) class names:
/* Valid in Sass, INVALID in Native CSS */
.card {
&__header {
font-size: 1.25rem;
}
&--featured {
border: 2px solid gold;
}
}
Native CSS does not support string concatenation. In native CSS, & always represents a complete, parsed selector list wrapped in an implicit :is() selector. Writing &__header will fail because the browser does not treat the ampersand as a raw text token. To build modular components natively, you should write complete class selectors or use nested compound classes:
.card {
/* Match when .card also has .featured */
&.featured {
border: 2px solid gold;
}
/* Match descendant */
.card-header {
font-size: 1.25rem;
}
}
Nesting At-Rules: Media Queries and Container Queries
One of the most practical productivity gains unlocked by native nesting is the ability to nest at-rules directly inside selector blocks. Previously, responsive design often required writing isolated media queries at the bottom of a stylesheet, splitting a component’s styles across hundreds of lines of code.
Native nesting allows you to place media queries and container queries directly where the component is declared:
.navigation {
display: flex;
flex-direction: column;
gap: 1rem;
@media (min-width: 768px) {
flex-direction: row;
align-items: center;
}
@media (prefers-color-scheme: dark) {
background-color: #1a1a1a;
color: #f0f0f0;
}
}
When the browser evaluates this block, it automatically associates the at-rule conditions with the parent selector. This keeps all stylistic rules for a given element co-located, dramatically reducing cognitive overhead when maintaining or refactoring code.
Managing Specificity: The Power of :is() and :where()
Nesting handles visual hierarchy, but modern CSS also requires precise control over selector specificity. Historically, writing long descendant selectors created accidental specificity wars that forced developers to resort to !important. Modern CSS provides two complementary pseudo-classes designed to solve this: :is() and :where().
Both selectors take a comma-separated list of selectors and match any element that satisfies any item in that list. Furthermore, both are “forgiving” selector lists—if one selector in the list is invalid, the browser simply ignores it rather than invalidating the entire rule. The critical difference lies in how they handle specificity:
:is()adopts the specificity of the most specific selector in its argument list.:where()always has a specificity of exactly zero (0, 0, 0), regardless of what is passed inside it.
/* Using :is() to group selectors concisely */
:is(header, main, footer) p:hover {
color: #0066cc;
}
/* Using :where() for low-specificity CSS resets */
:where(h1, h2, h3, h4, h5, h6) {
margin-block-start: 0;
line-height: 1.2;
}
Because :where() carries zero specificity, styles declared with it can be overridden effortlessly by any standard class or element selector later in your stylesheet. This makes :where() the ideal tool for creating design system defaults, component base styles, and CSS resets that downstream developers can customize without fighting the cascade.
The Relational Selector: Unleashing the Might of :has()
For decades, web developers jokingly referred to a “parent selector” as the impossible holy grail of CSS. Layout changes based on child states routinely required JavaScript event listeners or state management libraries. The :has() pseudo-class has finally made this possible natively.
The :has() selector is a relational pseudo-class. It allows you to style an element based on its children, its descendants, or subsequent siblings that match a specific selector. In simple terms: it lets you style the parent based on what is happening inside it.
Real-World Scenario 1: Form Validation Styling
Imagine styling a form container based on whether an input inside it is invalid or focused. With :has(), this requires zero JavaScript:
.form-group {
padding: 1rem;
border: 1px solid #ccc;
border-radius: 6px;
transition: border-color 0.2s ease;
/* Highlight container when any input inside is focused */
&:has(input:focus) {
border-color: #0066cc;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.2);
}
/* Flag container with red border when input is invalid */
&:has(input:user-invalid) {
border-color: #d32f2f;
background-color: #fff8f8;
}
}
Real-World Scenario 2: Dynamic Card Layouts
Consider an editorial card component that should dynamically change its layout depending on whether an author provided a thumbnail image:
.article-card {
display: flex;
flex-direction: column;
gap: 1rem;
/* Switch to horizontal grid only if an image exists */
&:has(.thumbnail) {
@media (min-width: 600px) {
display: grid;
grid-template-columns: 200px 1fr;
}
}
}
Combining Native Nesting and Advanced Selectors
The true architectural breakthrough occurs when you combine native nesting, :has(), and specificity-control selectors inside a single component definition. This creates self-contained, highly resilient UI modules.
.media-card {
background: #ffffff;
border-radius: 8px;
padding: 1.5rem;
/* Target nested headings regardless of level without raising specificity */
:where(h2, h3, h4) {
margin-block-end: 0.5rem;
color: #111;
}
/* Style based on nested media presence */
&:has(video, iframe) {
background: #0f172a;
color: #f8fafc;
:where(h2, h3, h4) {
color: #ffffff;
}
}
/* Interactive hover states */
&:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.08);
}
}
Notice how self-des