Native CSS nesting has been in every major browser since December 2023: Chrome 112, Firefox 117 and Safari 17.2. If your users run anything updated since then, you can nest rules without SCSS, PostCSS or a build step.
I write small stylesheets by hand for tool pages, and nesting is the one preprocessor feature I missed. This post covers the syntax, the two places it differs from SCSS, a specificity trap that SCSS never had, and how to decide whether migrating is worth your time at all.
Nest rules inside rules, and use & wherever the child touches the parent
A nested rule sits inside another rule's block:
`css
.card {
padding: 1rem;
border-radius: 0.5rem;
background: white;
.title { font-size: 1.25rem; font-weight: 600; }
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
}
`
The browser reads that as three flat rules:
`css
.card { padding: 1rem; border-radius: 0.5rem; background: white; }
.card .title { font-size: 1.25rem; font-weight: 600; }
.card:hover { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); }
`
& stands for the parent selector. Leave it out and the nested selector gets a descendant combinator in front of it (.card .title). Put it in and the selector attaches to the parent element itself. So you need & for pseudo-classes (&:hover), pseudo-elements (&::before) and a second class on the same element (&.active).
The first version of the spec would not let a nested selector start with an element name, because the parser could not tell h2 { from a property. That restriction is gone. Chrome 120, Firefox 117 and Safari 17.2 all accept this:
`css
.card {
h2 { font-size: 1.5rem; }
p { margin: 0.5rem 0; }
}
`
If you still support something older than Chrome 120, write & h2 instead. It is harmless in new browsers and parses in the old ones.
Two things SCSS does that native nesting does not
& is not string concatenation. In SCSS, &__title inside .card produces .card__title. In native CSS, & is a selector, not text, so &__title is a parse error and the whole nested rule is dropped. If your codebase is BEM, the element and modifier classes stay flat:
`css
.card { padding: 1rem; }
.card__title { font-weight: 600; }
.card--featured { border: 2px solid gold; }
`
This is the single biggest reason teams keep SCSS, and nothing in the current spec changes it.
& under a selector list becomes :is(), and that changes specificity. SCSS expands a rule nested under .card, #promo into two separate rules, each with its own specificity. Native nesting wraps the parent in :is(.card, #promo), and :is() takes the specificity of its most specific argument. So the .card branch now carries the weight of an ID and overrides things it never used to:
`css
.card, #promo {
.title { color: navy; }
}
/ native: :is(.card, #promo) .title, specificity 1,1,0 for both branches /
/ SCSS: .card .title is 0,2,0 and #promo .title is 1,1,0 /
`
Keep IDs out of nested parent lists and this never bites you. With a single parent selector the specificity is identical to the flat version.
Nested at-rules behave the way SCSS taught you: a @media block inside .card applies those declarations to .card inside that media query. There is no depth limit either, but three levels is where readability ends. Past that the selectors get long and the overrides get hard to reason about.

The three patterns that cover most of what I write
Component scoping, element states and a responsive tweak inside the component. That is nearly all of it.
`css
.nav {
display: flex;
gap: 1rem;
a { text-decoration: none; color: inherit;
&:hover { color: var(--color-primary); }
&.active { font-weight: 600; }
}
}
`
`css
.button {
padding: 0.5rem 1rem;
background: var(--color-primary);
&:hover { background: var(--color-primary-hover); }
&:active { transform: scale(0.98); }
&:disabled { opacity: 0.5; cursor: not-allowed; }
}
`
`css
.grid {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
@media (min-width: 768px) { grid-template-columns: repeat(2, 1fr); }
@media (min-width: 1024px) {
grid-template-columns: repeat(3, 1fr);
}
}
`
The media query living inside the component is the part I like most. Every rule that touches .grid is in one block, so nobody scrolls to the bottom of the file to find the breakpoint override. Container queries nest the same way.
One ordering habit saves a surprise: put a rule's own declarations first and the nested rules after them. Declarations written after a nested rule used to be hoisted above it, which changed the cascade order. Browsers since late 2024 (Chrome 130, Firefox 132, Safari 18.2) keep them in place, but if the declarations come first both behaviors give the same result and you never have to think about it.
Migrating off SCSS: decide first, then move one file at a time
List which SCSS features the codebase actually uses, because that list decides everything:
- Nesting: native CSS covers it, with the two exceptions above
- Variables: CSS custom properties, which also work at runtime, so theming gets easier
lighten()anddarken():color-mix()covers the common cases- Mixins with logic, loops,
@if: no native equivalent - BEM concatenation (
&__element,&--modifier): no native equivalent
If the list stops after the first three, migrate. If it includes the last two at any scale, keep SCSS and use native nesting only in new, standalone stylesheets. A hybrid is fine; every build tool I have used handles .scss and .css in the same project.
When you do migrate, do it per file. Rename, convert the variables, flatten any &__ selectors, load the page, move on. A big-bang rewrite of a stylesheet that already works buys you nothing.
If you must support a browser older than the ones above, keep a flattening step in the build: postcss-nesting or Lightning CSS turn nested rules into flat ones for the targets you name. That is a lighter dependency than SCSS and it can be removed later without touching the source.
For a small hand-written stylesheet I run the result through the CSS Minifier. It strips comments and whitespace and leaves the nesting alone, which is what you want: the browser does the flattening, not the minifier.
List which SCSS features the codebase actually uses, because that list decides everything: - Nesting: native CSS covers it, with the two exceptions above - Variables: CSS custom properties, which also work at runtime, so theming gets easier - `lighten()` and `darken()`: `color-mix()` covers the common cases - Mixins with logic, loops, `@if`: no native equivalent - BEM concatenation (`&__element`, `&--modifier`): no native equivalent If the list stops after the first three, migrate.
What the browser does with nested rules
Nothing you can measure, in terms of speed. The parser expands nested rules into their flat equivalents once, at parse time, and the style engine never sees the difference. The source file is a little smaller because parent selectors are not repeated; after gzip that difference is close to zero.
DevTools in Chrome, Firefox and Safari show the rules in their nested form in the Styles panel, so you see the hierarchy while inspecting. No source maps, which is one less thing to configure than with SCSS.
The cascade is unchanged. A nested .card .title and a flat .card .title have the same specificity and the same source order, with the :is() exception described above.
@scope is the companion feature, and it is no longer experimental. Chrome 118, Safari 17.4 and Firefox 128 all ship it, so since mid-2024 every current browser has it. It limits a block of rules to a subtree, with an optional lower boundary:
`css
@scope (.card) to (.card-footer) {
p { color: #333; } / paragraphs inside .card, but not inside .card-footer /
}
`
Nesting inside @scope gives you component-level styling without CSS-in-JS. I have only used it on small pages, so I will not claim it scales; on a page with a handful of components it does exactly what it says.

FAQ
Can I use native CSS nesting in production today?
Yes. Chrome and Edge 112+, Firefox 117+ and Safari 17.2+ support it, which means every browser updated since December 2023. In an older browser the nested blocks are treated as invalid declarations and dropped, while the parent rule's own declarations still apply. If those browsers matter to you, flatten at build time with postcss-nesting or Lightning CSS.
Should I stop using SCSS?
Only if you use it for nesting and variables. If you rely on mixins with logic, loops or &__ BEM concatenation, SCSS still earns its place. Running both is normal: native nesting in new stylesheets, SCSS left alone where it works.
Does native nesting work with Tailwind?
Yes. Tailwind v4 runs your own CSS through Lightning CSS, which understands nesting, so a nested component stylesheet next to your utility classes needs no extra plugin. On Tailwind v3, add tailwindcss/nesting to the PostCSS chain.
Is there a maximum nesting depth?
Not in the spec. In practice, three levels. If you need a fourth, the component wants splitting, not deeper nesting.
### Can I use native CSS nesting in production today.
JSON Guide: Format, Validate, and Convert JSON Files
JSON guide for developers: syntax rules, common parse errors, formatting and schema validation, plus how to convert between JSON and CSV files.
Base64, URL Encoding & HTML Entities Explained
Encode and decode Base64, URLs, and HTML entities in your browser. Learn when to use each format, with clear examples and free converter tools.
Regular Expressions for Beginners: A Practical Guide
Learn regular expressions from scratch: basic syntax, character classes, quantifiers, and practical patterns for matching emails, URLs, and phone numbers.
