Un-Sass'ing My CSS: Native CSS Nesting
On This Page:
One of the features I would miss most when moving away from Sass would be nesting. There's something wonderfully clean about grouping related selectors together, keeping your code organised and reducing repetition. For years, this was the realm of preprocessors like Sass, Less, and PostCSS, but now, CSS nesting has landed in the browser, and it's time to revisit it as a core part of how we write modern CSS.
What is CSS Nesting?
CSS nesting allows you to write child selectors within a parent selector, mirroring the DOM structure and reducing repetition. Instead of writing out full selectors each time, you nest rules and let the browser expand them into their final form.
Here's a simple example:
.card {
padding: 1rem;
background-color: #f5f5f5;
.card-header {
font-size: 1.25rem;
font-weight: bold;
}
.card-body {
margin-top: 0.5rem;
}
}The browser expands this into:
.card {
padding: 1rem;
background-color: #f5f5f5;
}
.card .card-header {
font-size: 1.25rem;
font-weight: bold;
}
.card .card-body {
margin-top: 0.5rem;
}You can nest as deeply as your structure requires (browsers support very deep nesting), though in practice you should keep nesting reasonably shallow to maintain code readability and avoid unnecessary specificity buildup.
Browser Support
CSS nesting has solid support across modern browsers as of 2026. Chrome, Edge, Firefox, and Safari all support it. You can check caniuse.com for more complete details.
Progressive Enhancement
CSS nesting could be another good place to think carefully about progressive enhancement. A browser that doesn't understand nesting won't just ignore the nested rules it will drop them entirely, which could mean missing :hover states, missing responsive adjustments, or missing theme variants. The base styles will still render, but the nested behaviour won't.
For most projects in 2026, your audience probably supports nesting. Chrome, Edge, Firefox, and Safari all ship with it. But if you need to support older browsers (enterprise environments, older Android WebViews, etc), you have two options. Write a layered approach with @supports (selector(&)) as a feature query, or stick with something like Sass if you want to nest by compiling down for older targets.
How Nesting Works in the Browser
When a browser parses nested CSS, it expands the nested selectors relative to the parent selector. For simple cases, it's straightforward:
.card {
.card-header {
font-size: 1.25rem;
}
}This becomes:
.card .card-header {
font-size: 1.25rem;
}However, when the parent selector has multiple selectors or complex patterns, the browser uses the :is() pseudo-class to handle the expansion. This is important because it affects specificity calculations.
For example, this:
.card,
.panel {
.header {
font-size: 1.25rem;
}
}Gets parsed as:
:is(.card, .panel) .header {
font-size: 1.25rem;
}This matters because :is() uses the highest specificity from its selector list for specificity calculations. If your parent is .card, #featured {}, the :is() takes the specificity of the ID, even if the nested rule only applies to .card.
For example:
#featured,
.card {
.header {
color: red;
}
}Becomes:
:is(#featured, .card) .header {
color: red;
}The .header inside .card now has ID-level specificity (1,1,0). This can catch you off guard when debugging styles that won't override as expected.
You can explicitly reference the parent with &, or omit it—the browser treats .parent { .child {} } as if you'd written & .child, creating a descendant relationship.
The & becomes essential when you need a different relationship, like pseudo-classes where no space is allowed (.button { &:hover {} } becomes .button:hover {}), or when reversing the selector order (.theme-dark & {} becomes .theme-dark .parent {}).
The Ampersand (&)
The & symbol is your key to manipulating how nesting works. It represents the parent selector and lets you compose more complex selector patterns. While you can omit & for simple descendant selectors (the browser treats .parent { .child {} } as if you'd written & .child), you'll need it explicitly for pseudo-classes, compound selectors, and other patterns where the relationship isn't a straightforward parent-child descendant.
You can use & to create pseudo-class selectors:
.button {
padding: 0.5rem 1rem;
background-color: #007bff;
&:hover {
background-color: #0056b3;
}
&:active {
background-color: #003d7a;
}
}This expands to:
.button {
padding: 0.5rem 1rem;
background-color: #007bff;
}
.button:hover {
background-color: #0056b3;
}
.button:active {
background-color: #003d7a;
}You can also use & to create compound selectors, which is handy for state classes—but with an important caveat about BEM patterns.
The key limitation: Native CSS nesting does not support Sass-style selector concatenation like &--primary or &__element. This is because in native CSS, selectors are object references, not strings. Sass treats them as strings and can concatenate them together. CSS cannot.
This means you can't write:
.button {
&--primary {
/* This won't work */
}
}Instead, you could use a compound selector with the full class name:
.button {
&.button--primary {
background-color: #007bff;
}
&.button--secondary {
background-color: #6c757d;
}
}Which expands to:
.button {
padding: 0.5rem 1rem;
}
.button.button--primary {
background-color: #007bff;
}
.button.button--secondary {
background-color: #6c757d;
}Or, if you're working with BEM element selectors, keep them at the root level:
.button { ... }
.button__icon { ... }
.button__label { ... }For historical context on BEM with Sass (where concatenation does work), see Even Easier BEM-ing with Sass 3.3.
Specificity and CSS Nesting
Here's where things get a little interesting. CSS nesting follows the same specificity rules as traditional CSS, with one important detail: the & nesting selector calculates its specificity like the :is() pseudo-class it uses the highest specificity from the parent selector list, not the sum of all the selectors.
Understanding Specificity with Nesting
Specificity is calculated as a tuple, (ID count, class count, element count).
For example:
- A single class selector:
(0,1,0) - A single element selector:
(0,0,1) - An ID selector:
(1,0,0)
When you nest, you stack these together just as you would with a descendant selector.
Here's the key detail: the & selector itself takes the highest specificity from its parent rule list. For example, if the parent is .foo, #bar {}, the & has the specificity of #bar (1,0,0), even when you nest rules inside .foo.
.card {
/* (0,1,0) - one class */
}
.card .card-header {
/* (0,2,0) - two classes */
}
.card .card-header h1 {
/* (0,2,1) - two classes, one element */
}In nested syntax, this looks cleaner, but the math is identical:
.card {
.card-header {
h1 {
/* Still (0,2,1) */
}
}
}While nesting can make your code more readable, deeper nesting means higher specificity, which can make overriding styles harder later. One quirk: nesting inside a selector list with an ID (like .card, #featured { .header {} }) wraps the list in :is(), lifting specificity to ID level. Keep this in mind if you spot unexpectedly high specificity when debugging.
CSS Nesting vs Sass Nesting
At first glance, CSS nesting and Sass nesting may look identical. You can nest selectors, use the ampersand to reference parents, and keep your code organised.
But, there are some important differences worth understanding.
Syntax Differences
Sass nesting tends to feel more forgiving because it's a preprocessor, it compiles your code before the browser ever sees it, giving it more room to interpret and transform what you write.
CSS nesting, being native browser code, has to follow stricter parsing rules.
Sass example:
.card {
padding: 1rem;
.card-header {
font-size: 1.25rem;
}
&--dark {
background-color: #333;
}
// You can also place & in different positions
.theme-dark & {
border-color: #666;
}
@media (max-width: 768px) {
padding: 0.5rem;
}
}CSS nesting equivalent:
.card {
padding: 1rem;
.card-header {
font-size: 1.25rem;
}
&.card--dark {
background-color: #333;
}
.theme-dark & {
border-color: #666;
}
@media (max-width: 768px) {
padding: 0.5rem;
}
}They look similar, but CSS nesting has stricter parsing rules.
Important Differences
Sass has variables, mixins, and functions
Sass can do math, store values, reuse chunks of code with mixins, and apply conditional logic. CSS nesting is purely structural—it doesn't add any of these capabilities. That said, CSS has its own equivalents: calc() for math, custom properties for values, functions like color-mix(), and @when/@else for conditionals. Mixins are being actively developed in the CSS spec, though they're not yet available cross-browser.
// Sass can do this
$spacing: 1rem;
.card {
// Sass will generate these values when compiling
padding: $spacing * 2;
// Sass gives you mixins to
@include respondTo('mobile') {
padding: $spacing;
}
}:root {
--spacing: 1rem;
}
.card {
/* CSS Will calculate this value in the browser */
padding: calc(var(--spacing) * 2);
/* CSS doesn't have mixins (yet) */
@media (max-width: 768px) {
padding: var(--spacing);
}
}Nesting depth and specificity are more visible in CSS
Nesting in both Sass and CSS increases specificity with each level, it's not unique to native CSS. The Sass documentation itself warns "Be aware that overly nested rules will result in over-qualified CSS that could prove hard to maintain and is generally considered bad practice." The difference is that with Sass, you might not see the compiled output regularly, so it could easier to lose track of how deep you've nested.
With native CSS, what you write is what the browser sees, hopefully making specificity issues more visible.
A Practical Example: Building a Component
Let's build a simple component using CSS nesting to show how it all comes together.
:root {
--alert-padding: 1rem;
--alert-border-radius: 0.25rem;
--alert-margin-bottom: 1rem;
--alert-icon-margin: 0.5rem;
--alert-title-margin: 0.25rem;
--alert-close-size: 1.5rem;
--alert-success-bg: #d4edda;
--alert-success-border: #c3e6cb;
--alert-success-text: #155724;
--alert-warning-bg: #fff3cd;
--alert-warning-border: #ffeaa7;
--alert-warning-text: #856404;
--alert-danger-bg: #f8d7da;
--alert-danger-border: #f5c6cb;
--alert-danger-text: #721c24;
}
.alert {
padding: var(--alert-padding);
border-radius: var(--alert-border-radius);
border: 1px solid transparent;
margin-bottom: var(--alert-margin-bottom);
/* Modifier variants use & for state changes */
&.alert--success {
background-color: var(--alert-success-bg);
border-color: var(--alert-success-border);
color: var(--alert-success-text);
}
&.alert--warning {
background-color: var(--alert-warning-bg);
border-color: var(--alert-warning-border);
color: var(--alert-warning-text);
}
&.alert--danger {
background-color: var(--alert-danger-bg);
border-color: var(--alert-danger-border);
color: var(--alert-danger-text);
}
/* Responsive adjustments */
@media (max-width: 640px) {
padding: calc(var(--alert-padding) * 0.75);
}
}
/* Class selectors stay at the component level */
.alert__icon {
margin-right: var(--alert-icon-margin);
display: inline-block;
}
.alert__title {
font-weight: bold;
margin-bottom: var(--alert-title-margin);
}
.alert__close {
background: none;
border: none;
cursor: pointer;
font-size: var(--alert-close-size);
float: right;
/* Nest pseudo-classes for interactive states */
&:hover {
opacity: 0.7;
}
}Notice how we use nesting for modifier variants (&.alert--success) and media queries, but keep the child class selectors (.alert__icon, .alert__title, .alert__close) at the component level. This could keep specificity low while still grouping related interactive behaviour with the component.
Nesting with Container Queries
CSS nesting becomes particularly powerful when combined with other modern CSS features like container queries. Here's a card component that adapts its layout based on the container's width, not the viewport. We'll use custom properties for consistent spacing and colors:
:root {
--card-gap: 1rem;
--card-padding: 1rem;
--card-border-color: #e0e0e0;
--card-border-radius: 0.5rem;
--card-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
--card-image-aspect: 16 / 9;
--card-image-radius: 0.25rem;
--card-title-size: 1.1rem;
--card-title-margin: 0 0 0.5rem 0;
--card-text-size: 0.9rem;
--card-text-color: #666;
--card-text-line-height: 1.5;
}
.card-wrapper {
container-type: inline-size;
}
.card {
display: grid;
grid-template-columns: 1fr;
gap: var(--card-gap);
padding: var(--card-padding);
border: 1px solid var(--card-border-color);
border-radius: var(--card-border-radius);
box-shadow: var(--card-shadow);
img {
aspect-ratio: var(--card-image-aspect);
object-fit: cover;
border-radius: var(--card-image-radius);
width: 100%;
}
h3 {
margin: var(--card-title-margin);
font-size: var(--card-title-size);
font-weight: 600;
}
p {
margin: 0;
font-size: var(--card-text-size);
color: var(--card-text-color);
line-height: var(--card-text-line-height);
}
@container (min-width: 400px) {
grid-template-columns: 200px 1fr;
gap: calc(var(--card-gap) * 1.5);
img {
aspect-ratio: 2 / 1;
}
}
}The card starts as a single column with the image above the text. When the card wrapper reaches 400px, it switches to a two-column layout with the image on the left and text on the right. Notice how the nesting keeps all the related rules together, making it clear how the component adapts.
This demonstrates an important advantage of CSS nesting: it lets you group related styles—including responsive adaptations—in one logical block rather than scattering them across multiple rules.
Accessibility: Reduced Motion Preferences
Seeing Kilian post about this opt-in animation technique was part of the inspiration for writing a third article in this Un-Sass'ing series (they've since written a longer article on it too).
It's also a great practical example of where nesting can genuinely earn its keep, handling user preferences like prefers-reduced-motion by keeping overrides grouped with the component. The example below uses an opt-in approach. Rather than adding a transition and then removing it for users who prefer reduced motion, we only add the transition when motion is acceptable.
No animation is the default.
:root {
--notification-bg: #007bff;
--notification-color: white;
--notification-padding: 1rem;
--notification-radius: 0.5rem;
--notification-position: fixed;
--notification-top: 1rem;
--notification-right: 1rem;
--notification-transform-start: translateX(400px);
--notification-transform-end: translateX(0);
--notification-transition-duration: 0.3s;
--notification-transition-easing: ease-out;
}
.notification {
position: var(--notification-position);
top: var(--notification-top);
right: var(--notification-right);
padding: var(--notification-padding);
background: var(--notification-bg);
color: var(--notification-color);
border-radius: var(--notification-radius);
transform: var(--notification-transform-start);
&.is-visible {
transform: var(--notification-transform-end);
}
@media (prefers-reduced-motion: no-preference) {
transition: transform var(--notification-transition-duration)
var(--notification-transition-easing);
}
}Everything related to the notification's behaviour lives in one place, and motion is treated as a progressive enhancement rather than something you would have to remember to turn off.
Theming with Data Attributes
Nesting keeps theme variations organised alongside your base styles. Custom properties make theming even more powerful by letting you define theme tokens that can be swapped out:
:root {
--prose-color: #333;
--prose-bg: #fff;
--prose-padding: 2rem;
--prose-link-color: #007bff;
--prose-link-hover: #0056b3;
--prose-code-bg: #f5f5f5;
--prose-code-padding: 0.125rem 0.25rem;
--prose-code-radius: 0.25rem;
--prose-dark-color: #e0e0e0;
--prose-dark-bg: #1a1a1a;
--prose-dark-link: #4dabf7;
--prose-dark-link-hover: #74c0fc;
--prose-dark-code-bg: #2a2a2a;
}
.prose {
color: var(--prose-color);
background: var(--prose-bg);
padding: var(--prose-padding);
a {
color: var(--prose-link-color);
text-decoration: underline;
&:hover {
color: var(--prose-link-hover);
}
}
code {
background: var(--prose-code-bg);
padding: var(--prose-code-padding);
border-radius: var(--prose-code-radius);
}
[data-theme='dark'] & {
color: var(--prose-dark-color);
background: var(--prose-dark-bg);
a {
color: var(--prose-dark-link);
&:hover {
color: var(--prose-dark-link-hover);
}
}
code {
background: var(--prose-dark-code-bg);
}
}
}You can see at a glance how each element adapts to dark mode rather than hunting through separate rule blocks.
Interactive States with :focus-within
Nesting makes complex interactive patterns much clearer.
Nesting and custom properties help maintain consistency across interactive elements.
:root {
--search-border: 2px solid transparent;
--search-border-focus: 2px solid #007bff;
--search-radius: 0.5rem;
--search-bg: #f5f5f5;
--search-bg-focus: #fff;
--search-padding: 0.5rem;
--search-transition: border-color 0.2s;
--search-icon-color: #666;
--search-icon-focus: #007bff;
--search-button-opacity: 0.5;
--search-button-visible: 1;
--search-button-transition: opacity 0.2s;
}
.search-form {
display: flex;
border: var(--search-border);
border-radius: var(--search-radius);
background: var(--search-bg);
padding: var(--search-padding);
transition: var(--search-transition);
&:focus-within {
border: var(--search-border-focus);
background: var(--search-bg-focus);
.search-icon {
color: var(--search-icon-focus);
}
.search-button {
opacity: var(--search-button-visible);
pointer-events: auto;
}
}
& input {
border: none;
background: transparent;
flex: 1;
outline: none;
}
.search-icon {
color: var(--search-icon-color);
transition: color 0.2s;
}
.search-button {
opacity: var(--search-button-opacity);
pointer-events: none;
transition: var(--search-button-transition);
}
}When any child receives focus multiple coordinated style changes happen and you can see the entire interaction pattern in one block of CSS.
Modern Selector Patterns with :has()
The :has() selector combined with nesting unlocks powerful parent-child relationships:
.article {
padding: 2rem;
&:has(> img) {
display: grid;
grid-template-columns: 200px 1fr;
gap: 2rem;
img {
grid-row: 1 / -1;
}
}
&:has(.quote) {
border-left: 4px solid #007bff;
padding-left: 2rem;
}
&:not(:has(h2)) {
& p:first-of-type::first-letter {
font-size: 3em;
float: left;
margin-right: 0.1em;
line-height: 0.9;
}
}
}You're describing layout behaviour based on the content present. Nesting keeps these conditional patterns in one place and easily readable.
When to Nest and When Not To
Nesting is really useful when you're adding behaviour or context to a selector, pseudo-classes, media query, or theme variants etc. If you're using BEM or similar methodologies, keep element selectors like .button__icon at the root level. The class name already describes the relationship, so nesting just adds specificity without adding clarity.
Nest when
- Handling pseudo-classes, pseudo-elements, or state variations
- Grouping media queries, container queries, or feature queries with their components
- Handling user preferences like
prefers-reduced-motionorprefers-color-scheme - Creating theme variants or data-attribute patterns
- The nesting improves readability without adding unnecessary specificity
Avoid nesting when
- Using BEM or similar methodologies where classes are already specific
- It would create overly specific selectors that are hard to override
- Nesting more than 2-3 levels deep
- Simply mirroring the DOM structure without adding value
A simple rule I still follow from writing Sass for ~12 years – if you're nesting 3+ levels deep regularly, split it into smaller components. Although it's not a hard and fast rule, on occasion it can be broken.
CSS Nesting as Part of Modern CSS
CSS nesting is one part of a larger "Un-Sass'ing" toolkit. Combined with CSS custom properties, color-mix(), and other modern features, nesting gives you a lot of what made Sass appealing without needing a preprocessor.
You lose some of the programmatic power of Sass (variables, mixins, functions) that gets compiled to 'simpler' CSS, but you gain the ability to write cleaner, more organised CSS that the browser understands natively.
The key is understanding when you're reaching for nesting and why. Use it where it makes sense and to keep your code organised, but don't let it become a shortcut that will create specificity problems down the line.