You npm install sass, configure a loader, wait for compilation — all so you can write .card { .title { ... } }. CSS has done this natively since 2023. Chrome 120, Safari 17.2, Firefox 117, Edge 120. Every evergreen browser ships it. Your build step is compiling something the browser already understands. CSS is replacing JavaScript dependencies too — the same native-first shift is happening across the platform.
Can CSS nesting replace Sass? Yes for component scoping, responsive modifiers, and runtime theme variants — that covers roughly 80% of Sass nesting use cases. No for BEM selector concatenation and build-time token generation from design system maps. Native CSS nesting is production-ready in every evergreen browser since 2024.
If it’s been ready for years, why is everyone still compiling? Because the gotchas aren’t obvious until production. This css nesting tutorial covers the 3 patterns that prove you can drop your preprocessor, the 2 dealbreakers where you can’t, and the migration path between them.
The Syntax Gap Closed While You Weren’t Looking
Early native CSS nesting required & before element selectors. That was the friction point — your Sass didn’t copy cleanly. Relaxed nesting syntax shipped in Chrome 120 and Safari 17.2, eliminating that last barrier. You no longer need & before element selectors.
Here’s a Sass block:
.card {
padding: 1.5rem;
h2 { font-size: 1.25rem; }
p { color: var(--text-muted); }
}
Native CSS. Identical:
.card {
padding: 1.5rem;
h2 { font-size: 1.25rem; }
p { color: var(--text-muted); }
}
No transpilation. No build step. The & still exists for compound selectors (.card { &.featured { ... } }) and parent references — same as Sass. It’s not gone. It’s optional where you’d expect it to be.
If you dropped IE11 support — you did — css nesting browser support is universal. But toy examples don’t prove anything. Does it handle real component patterns?
Pattern 1: Component Scoping Without a Build Step
Here’s a card component with base styles, nested children, pseudo-states, and a responsive breakpoint — all inside one block:
.card {
background: var(--surface);
border-radius: 0.5rem;
padding: 1.5rem;
.card-header {
font-weight: 600;
margin-bottom: 0.75rem;
}
.card-body {
color: var(--text-muted);
line-height: 1.6;
}
&:hover {
box-shadow: 0 4px 12px rgb(0 0 0 / 0.15);
transition: box-shadow 0.2s ease;
}
&:focus-visible {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
@media (width >= 768px) {
padding: 2rem;
.card-header { font-size: 1.25rem; }
}
}
That’s component scoping without CSS Modules, without Shadow DOM, without a build step. The nesting creates a visual and logical boundary around the component. Every style that belongs to .card lives inside .card. The :hover with a transition — the pattern that covers 60% of real Sass nesting — works exactly as you’d write it in SCSS.
If you’re already comfortable with container queries for component-level responsiveness, nesting is the structural complement. Scoping handles the selectors. Container queries handle the layout.
Component scoping works. But what about the responsive styles scattered across three breakpoint blocks in different parts of your file?
Pattern 2: Responsive Modifiers That Live Where They Belong
The “hunt for the breakpoint” problem: you have .sidebar styles at the top of the file, then @media (width >= 768px) { .sidebar { ... } } 200 lines down, then another @media (width >= 1024px) breakpoint even further. You end up grep-ing your own stylesheet to find all the responsive behavior for one component.
Nesting fixes this:
.sidebar {
display: none;
@media (width >= 768px) {
display: block;
width: 280px;
}
@media (width >= 1024px) {
width: 320px;
}
@container layout (min-width: 900px) {
grid-column: span 2;
}
}
One selector block. One component. All its responsive behavior in one place. No more Ctrl+F for breakpoints.
This is where native CSS nesting actually beats Sass. @container queries are runtime constructs — the browser evaluates them against actual element sizes, not viewport dimensions. Sass can nest @media, but it cannot do @container because containers don’t exist at build time. You’re nesting something your preprocessor literally cannot process.
Scoping and responsive, done. But can nesting handle theming without Sass variables?
Pattern 3: Runtime Theme Variants (Sass Can’t Do This)
Sass $variables are dead after compilation. They become hardcoded values in the output CSS. You can’t change them at runtime without rebuilding.
CSS custom properties are live. Combine them with nesting:
.btn {
background: var(--btn-bg);
color: var(--btn-text);
&:hover {
background: var(--btn-bg-hover);
}
}
[data-theme="dark"] {
--btn-bg: #1a1a2e;
--btn-bg-hover: #16213e;
--btn-text: #e2e8f0;
}
Theme switching without rebuilding. Without JavaScript toggling classes on every element. Without duplicating style blocks.
The :has() bonus: nest it inside a component for parent-aware styling that Sass literally cannot replicate.
.form-group {
border: 1px solid var(--border);
&:has(input:invalid) {
border-color: var(--error);
}
&:has(input:focus) {
border-color: var(--primary);
}
}
The parent styles itself based on its children’s state. At runtime. No JavaScript. No Sass.
Three patterns — component scoping, responsive modifiers, theme variants. That covers roughly 80% of what Sass nesting does. But the article promised dealbreakers. Here’s the first one most developers don’t see coming.
The :is() Specificity Trap That Will Bite You
Native CSS nesting wraps parent selectors in :is(). This matters when you nest inside a comma-separated selector list.
In Sass, this:
.card, #featured {
h2 { color: blue; }
}
Compiles to .card h2, #featured h2 — two separate rules with different specificities. The .card h2 rule has class-level specificity. The #featured h2 rule has ID-level specificity.
In native CSS, the same nesting produces :is(.card, #featured) h2. And :is() takes the specificity of its most specific argument. Now both .card h2 and #featured h2 have ID-level specificity — because #featured is in the list.
Your override rule that should win by specificity? It doesn’t. In Sass this worked. In native CSS it breaks.
The fix: avoid nesting inside comma-separated selector lists when the selectors have different specificity weights. If you need both, use @layer to manage the cascade explicitly or write them as separate blocks.
This is the one thing that will trip you in production if you blindly copy Sass nesting. It’s not a dealbreaker — it’s a one-time lesson. But there are two actual dealbreakers.
2 Edge Cases Where You Still Need a Preprocessor
Edge case 1: BEM concatenation. &__element and &--modifier don’t work in native CSS. The & is a live selector reference, not a string. You can’t build .card__title from .card { &__title { } }. The browser doesn’t concatenate — it errors silently or matches nothing.
If your codebase is heavily BEM, you have two options: keep Sass for those files, or migrate to flat class selectors (.card-title instead of .card__title) which nest cleanly. The flat approach works — and a vanilla CSS approach works at scale too. The migration is tedious. Be honest about whether it’s worth the churn.
Edge case 2: Build-time token generation. Sass @each loops over design token maps to generate utility classes, spacing scales, color palettes. CSS has no equivalent. If your design system generates hundreds of utility classes from a token file, Sass — or a build script — is still the right tool.
The honest verdict: if you use BEM concatenation or generate tokens from maps, keep your preprocessor for those files. Use native CSS nesting for everything else. They coexist fine. .scss files for the edge cases, .css files for everything else, same project.
Now the practical question: how do you actually migrate?
The 5-Step Migration
- Audit your Sass files. Grep for
&__and@each— those are the two edge cases. Everything else is a migration candidate. - Replace
$variableswith CSS custom properties.$primary: #4ade80becomes--primary: #4ade80. This is the largest mechanical change but the simplest conceptually. - Copy your nesting blocks to
.cssfiles. Relaxed syntax means most Sass nesting works as-is. Test in the browser — not a build step. - Handle BEM. Rename
&__elementto flat classes, or keep those specific files in Sass. - Remove the Sass build step from the files that no longer need it. If you’re using Vite, it already handles
.cssfiles with zero config.
That npm install sass and build configuration you’ve been maintaining? For most of your codebase, you can delete it today. The three patterns above prove the browser handles it. The two edge cases tell you exactly which files to keep.
Native CSS nesting isn’t coming. It’s been here since 2023. The question isn’t whether to migrate — it’s how much of your Sass you can drop this week.