Another comparison between Angular 22.2 and React/Next.js. The previous one covered @boundary, the new block for template errors: Template errors: React and Next.js vs Angular 22.2's @boundary.
Browsers now support nested CSS without a preprocessor:
.card {
padding: 1rem;
.title { font-weight: 600; }
&:hover { outline: 1px solid; }
}
Cross-browser support for the relaxed nesting syntax arrived in Firefox 117, Chrome 120 and Safari 17.2, according to MDN's compatibility data. Chrome 112 and Safari 16.5 already handled nested class selectors like .title; the later versions added nested rules that start with a tag, like h2 {}. Since June 2026, nesting is Baseline widely available. (Baseline "newly available" means it works in the latest Chrome, Edge, Firefox and Safari; "widely available" comes 30 months later.)
Nesting was probably one of the most common reasons to use Sass (the language behind your .scss files). Now it's native. The question is what happens when you combine it with each framework's style isolation.
TL;DR
- Next.js: CSS Modules rename every local class, nested ones included, so nesting just works; the build also flattens it.
-
Angular: the CLI flattens nesting before encapsulating, so it works in component styles. Avoid nesting under
::ng-deep, and know the 22.2:hosttrap if raw nesting reaches the compiler. -
Sass: still worth it for mixins, loops, BEM names with
&__, breakpoint variables and custom Angular Material themes. For the rest, native CSS is enough.
React and Next.js: you pick the isolation
React doesn't scope styles. className works like the HTML class attribute (react.dev), so a CSS class applies to the whole page. The Next.js styling docs list several options:
- Tailwind CSS: recommended "for most styling needs".
-
CSS Modules: a
.module.cssfile generates unique class names, so you can use.cardin different files without collisions. - Global CSS: for what is truly global.
- External stylesheets, Sass and CSS-in-JS: the other options.
import styles from './card.module.css';
<div className={styles.card}>
<h2 className={styles.title}>…</h2>
</div>
Isolation comes from renaming the classes, and that covers nesting too: the build renames every local class it finds (anything not wrapped in :global()), nested or not. We put the .card snippet from the top in a card.module.css and ran next build (Next.js 16.4 with default settings; Turbopack, the default bundler, compiles CSS with Lightning CSS). This is the output, reformatted:
.mXu7RW_card { padding: 1rem; }
.mXu7RW_card .mXu7RW_title { font-weight: 600; }
.mXu7RW_card:hover { outline: 1px solid; }
Both classes got the file's prefix, and the build also flattened the nesting. It flattened it even with a browserslist of modern browsers only: Turbopack lowers nesting regardless of your targets.
Angular: emulated encapsulation by default
Angular scopes each component's styles with no setup. Emulated encapsulation generates a unique HTML attribute per component (_ngcontent-X), adds it to the elements in its template, and inserts it into every selector in its styles. It adds a second one, _nghost-X, to the component's own host element (like <app-card>), and that's what :host selects.
Up to Angular 22.1, that rewrite didn't process style rules nested inside other style rules (rules inside at-rules such as @media were fine), as the description of PR #50693 says. With the .card snippet from the top, .title got no attribute, so it also styled the .title of any child component inside the card.
So the CLI took a shortcut: since @angular/build 19.2.8, released in April 2025, the default esbuild-based builders always flatten nesting in component styles before they reach the compiler (commit). .card { .title {} } reaches it as .card .title {}. It doesn't depend on which browsers you support: we tested a 22.2.1 project with a .browserslistrc of modern Chrome only, and it still flattened.
Angular 22.2: the fix, more than three years later
Angular 22.2 scopes nested rules when they reach the compiler. If the parent selector uses ::ng-deep (Angular's deprecated escape hatch that turns encapsulation off for the rest of the selector), nested rules aren't encapsulated: :host ::ng-deep { .foo {} } behaves like :host ::ng-deep .foo {} (commit). In 22.2.0 and 22.2.1, a nested rule under ::ng-deep could still get the attribute when an earlier rule also used ::ng-deep; 22.2.2 fixed it (commit).
That exception only helps when raw nesting reaches the compiler. In a default CLI project, don't nest under ::ng-deep at all: the CLI's flattening turns :host ::ng-deep { .foo {} } into :is() .foo {}, which matches nothing (issue #58996, open since November 2024).
The first attempt dates from June 2023. A month later, Google's internal tests found a failure in a file with base64 images; the PR was marked as blocked in 2024 and closed in 2025. The second attempt was merged in August 2026 and shipped with Angular 22.2 in September: more than three years.
In a default CLI project you'll barely notice the fix, because your component styles already arrived flattened. It matters when raw nested CSS reaches the compiler: JIT components whose styles isn't a string literal (the CLI can't see those at build time), dev builds on the webpack builders (@angular-devkit/build-angular, which don't force flattening; production builds may still flatten it when minifying for your browser targets), or libraries built with ng-packagr that target only modern browsers, which ship nesting as is.
The :host and & trap
In those cases, 22.2 brings a new side effect. We ran the same CSS through the compiler from 22.1.8 and from 22.2.0:
/* what you write */
:host { &.active { color: red; } }
/* 22.1.8: works, the browser resolves [_nghost-X].active */
[_nghost-X] { &.active { color: red; } }
/* 22.2.0: no match, the host doesn't have _ngcontent-X */
[_nghost-X] { &.active[_ngcontent-X] { color: red; } }
We confirmed it in Chromium with a JIT build of 22.2.1 whose styles was a constant, so the CLI couldn't flatten it: the rule stops applying. The same happens with :host { &:hover {} }. It's already reported as issue #71050, still unfixed in 22.2.2 (a fix, PR #71152, is open). Use the function form of :host instead: :host(.active) and :host(:hover), which is also the form that works with Shadow DOM. Your own component styles in a default CLI build (AOT or JIT with literal styles) aren't affected, because the CLI flattens them first.
This story shows how fragile it is to emulate encapsulation by rewriting selectors: every new CSS syntax has to be taught to the compiler, and every fix can break another combination.
Do you still need SCSS?
Sass is the most common preprocessor, so it's the one we compare. Less offers the same features discussed below (mixins with parameters, and loops through recursive mixins or each()), so the same reasoning applies. One practical difference: the Angular CLI compiles both (Less after you install the less package), while Next.js doesn't support Less out of the box (for Turbopack, its default bundler, it's "planned via plugins").
What Sass used to give you that is now native:
-
Nesting: native, with two differences.
&doesn't concatenate, so there's no BEM-style&__title, and its specificity is computed like:is()(MDN). -
Variables: custom properties (
--brand-color), which also change at runtime. But you can't use them in a media query condition:@media (min-width: var(--bp))doesn't work, while a Sass variable does. -
Math:
calc(),min(),max(),clamp()and trigonometric functions likesin()are Baseline widely available.round(),mod()andrem()are newer: Baseline newly available since May 2024, widely available from November 2026. -
Color functions:
color-mix()(Baseline widely available) can lighten or darken by mixing with white or black. For exact adjustments like Sass'scolor.adjust()(the old, now deprecatedlighten()anddarken()), there's relative color syntax, which works in all current major browsers (Baseline since September 2024) but won't be widely available until March 2027.
What is still Sass:
Mixins: reusable blocks of styles with parameters.
@mixin truncate($lines) {
display: -webkit-box;
-webkit-line-clamp: $lines;
-webkit-box-orient: vertical;
overflow: hidden;
}
.description { @include truncate(2); }
Loops: generating many rules from a list.
@each $name, $color in (ok: green, error: red, warning: orange) {
.badge-#{$name} { background: $color; }
}
Custom properties cover part of what a mixin did (a reusable value you can change per component), but CSS has no way to include a parameterized block inside another rule; the closest is a utility class that reads custom properties, which you have to add in the markup. There's also no way to generate rules in a loop. Native mixins (@mixin) aren't enabled by default in any browser (Chrome has them only as a prototype behind a flag), and @function, which returns a value rather than a block of declarations, ships only in Chrome and Edge 139+. And if you want a custom Angular Material theme through its supported theming API, you still need Sass (a file with the mat.theme mixin); without it, the supported alternative is one of the prebuilt CSS themes (guide).
Beyond emulated encapsulation
The platform has two native ways to keep styles in their place.
Shadow DOM, available today. Angular offers ViewEncapsulation.ShadowDom, which affects event propagation and content projection. Angular also registers each regular shadow root with its shared style host, so Angular-managed component styles are copied into it. ExperimentalIsolatedShadowDom skips that shared-style injection, although normal CSS inheritance, including custom properties, still crosses the shadow boundary (guide, implementation).
@scope, looking ahead. @scope limits styles to a DOM subtree. It works since Chrome 118, Firefox 146 and Safari 17.4 (Safari 26.0 to 26.3 didn't apply it to <input> and <textarea>; 26.4 fixed it), and it's Baseline newly available since March 2026. There's an open request to add an encapsulation mode based on it, and a team member replied that it's something they've been thinking about. It isn't on the roadmap, and some developers already switch Angular's encapsulation off and scope components with @scope by hand (example).
Our take
-
New project, React or Angular: native CSS is enough for most cases. Add a preprocessor if you need mixins, loops, BEM names built with
&__, breakpoints as variables in media queries, or a custom Angular Material theme. - You write nesting, the browser gets flat CSS. With default settings, both Next.js and the Angular CLI (in component styles) flatten nesting at build time, and both keep the styles isolated.
-
In Angular, you can already use native nesting in component styles, because the default CLI builder flattens it before encapsulating. The exception is nesting under
::ng-deep(#58996). If raw nesting reaches your compiler (non-literal JIT styles, webpack dev builds, some libraries), move to 22.2.2 or later and write host states with:host(...). On the webpack builders, moving to the esbuild-based@angular/build:applicationbuilder also avoids the problem, because it flattens first.
The underlying difference remains. React lets you choose the isolation mechanism (and choosing well is on you). Angular solves it for you (and adapting it to new CSS is on the framework, sometimes years late).











Top comments (0)