skip to content

A team removes Sass and rewrites its stylesheets with native CSS nesting. Which behaviours can change even when the nested source looks identical, and what would you check first?

level: seniorimportance: nice to knowfreq 30%

answer

  1. compiler versus browser selector
  2. string & versus real &
  3. one ID poisons the nested block
  4. invalid rules die silently
  5. specificity regressions render, never throw

basics

~10 s

Native nesting is not a drop-in Sass replacement: & is a real selector with :is()-like specificity rather than text concatenation, so overrides can silently change winner and BEM suffix rules are dropped as invalid.

solid answer

~50 s

Two classes of change. The loud one: anything Sass-only stops working — `&__title` suffixes, `@mixin`/`@include`, `@extend`, `$variables`, `//` comments, nested properties. Those produce invalid or unparseable CSS, so rules vanish and you find them fast. The quiet one is specificity: Sass emitted each branch of a parent selector list as its own selector, while native `&` scores like `:is()`, taking the highest specificity in the list. `#hero, .card { & p { } }` used to score (0,1,1) inside `.card` and now scores (1,0,1) everywhere, so overrides that used to win start losing with no error anywhere. I would first grep for `&` glued to an identifier, then for selector lists that mix IDs or heavy compounds with classes, then diff rendered pages visually — the specificity shifts show up as wrong colours and spacing, not as console output.

code

css · 13 lines
css
/* Same source, different meaning after dropping Sass */
#hero, .card {
  & p { color: red; }
}
/* Sass emitted:  #hero p (1,0,1)  and  .card p (0,1,1)
   Native scores: :is(#hero, .card) p -> (1,0,1) for both */

.card p { color: blue; }  /* (0,1,1): won before, loses now */

/* Restore the old weight without splitting the rule */
:where(#hero), .card {
  & p { color: red; }
}

go deeper

for a junior

Know that Sass and native nesting are not interchangeable: BEM suffix rules stop working entirely, and Sass features like variables and mixins have no plain-CSS equivalent.

for a middle

Explain the two failure classes — invalid selectors that get dropped, and the :is()-style specificity of the nesting selector — and show how a heterogeneous parent selector list causes the quiet one.

for a senior

Demonstrate a migration plan: static sweep for glued ampersands, audit of mixed-weight selector lists, visual diffing because specificity regressions never throw, and file-at-a-time cutover rather than big bang.

for a principal

Own the decision itself — weigh a shorter build pipeline and greppable selectors against losing mixins and generated rules, and set the browser-support floor, since unparsed nesting takes a whole block down rather than degrading.

## Why this is not a mechanical find-and-replace Sass nesting and CSS nesting look the same on the page and mean different things. Sass is a compiler: it expands nested source into flat selectors *before* the browser sees it, and `&` is a string. Native nesting is a browser feature: `&` is a selector evaluated against the DOM. Everything that follows comes from that one difference. ## The loud failures These break visibly, because the resulting CSS is invalid or meaningless: - **Suffix concatenation.** `&__title`, `&--large`, `&-active` form an illegal compound (`&` followed by a type selector) and CSS error handling discards the entire rule, silently but completely. Every BEM element and modifier rule must be written out in full. - **Sass-only syntax.** `$variables`, `@mixin`/`@include`, `@extend`, `@use`/`@forward`, Sass functions such as `darken()`, nested properties (`font: { size: 1rem; }`) and `//` comments have no native equivalent. Custom properties replace many variable uses — with the important difference that they are live, inherited, and readable at runtime rather than substituted at build time — but there is no shipped native mixin. - **Build-time math and loops.** Anything generated by `@each`/`@for` must become hand-written rules or be produced by some other tool. Because these fail as parse errors, a stylesheet linter or a devtools pass over the affected components finds them quickly. ## The quiet failure: specificity This is the one that reaches production. Given the same source: ```css #hero, .card { & p { color: red; } } ``` Sass emits `#hero p, .card p` — two selectors, scoring (1,0,1) and (0,1,1). Native nesting emits one rule whose nesting selector behaves like `:is(#hero, .card)`, so **every** match scores (1,0,1). A later, more specific-looking override: ```css .card p { color: blue; } /* (0,1,1) */ ``` used to win inside `.card` by source order and now loses by a full ID. Nothing warns you. The rule still exists, still matches, and simply stops taking effect. The pattern to hunt for is a **heterogeneous parent selector list** — one where the branches do not all have the same specificity. Homogeneous lists (`.card, .panel`) are unaffected. Where a heavy branch must stay, wrapping it in `:where()` restores the old score: ```css :where(#hero), .card { & p { color: red; } /* back to (0,1,1) */ } ``` ## Other behavioural differences - **No flattened artefact.** The nested source is what ships, so devtools show nesting rather than expanded selectors, and any tooling that greps compiled CSS for a selector string no longer finds one. - **Runtime support matters.** Nesting is resolved by the browser, so it needs engines that implement it — current Chrome, Safari, and Firefox since late 2023. A build that previously produced flat CSS for any browser now ships syntax an older engine cannot parse, and an unparsed nested rule takes its whole block down. - **The relaxed-parsing history.** Early implementations required a nested selector to start with a symbol, so hand-written examples and code copied from 2023 articles may include `& ` prefixes that are no longer required but are still correct. - **`&` is greppable, names are not.** Losing suffix concatenation is a real ergonomic cost, but it means a search for `.card__title` now finds its rule — which makes safe deletion easier in exactly the codebases where migration is being considered. ## How I would sequence the work 1. **Static sweep first.** Grep for `&` immediately followed by an identifier character; every hit is a rule that will be dropped. Fix them by writing full class names. 2. **Selector-list audit.** Grep for rules whose selector list contains an `#` alongside a class, or otherwise mixes weights, and decide per case: split the rule, drop the ID, or wrap the heavy branch in `:where()`. 3. **Feature inventory.** List every Sass feature in use — mixins and `@extend` need a design decision, not a rewrite rule, because there is no native replacement. 4. **Visual verification.** Specificity regressions do not throw; they render. Screenshot-diff the pages that matter, or walk the components whose selectors you touched, before trusting the migration. 5. **Migrate incrementally.** Native nesting and a preprocessor can coexist — Sass understands the syntax it shares — so a file-at-a-time move is safer than a big-bang rewrite. ## The judgment call underneath The question to ask is not "can we do this" but "what do we lose". A team migrating for build-simplicity gains a shorter pipeline and honest, greppable selectors; it loses mixins and generated rules, and takes on a one-time specificity risk that no compiler will flag. That trade is usually good for a design-token-based codebase already leaning on custom properties, and poor for one whose stylesheets are largely machine-generated from Sass loops.

  • Which of these two failure classes worries you more in production, and why?
    The specificity shift. Dropped rules from `&__title` are loud — the selector is invalid, the styling is visibly absent, and a lint pass or a single page load finds them. A specificity change produces a page that renders, just with the wrong rule winning, so it can pass review and reach users. It is also the one no tool flags for you.
  • Can native nesting and Sass coexist during a migration?
    Yes, and that is the safer path. Sass understands nesting syntax it shares with CSS, so you can move file by file, removing Sass-only constructs as you go, rather than doing a single cutover. The caveat is that while a file is still compiled, `&` is being resolved by Sass, so the specificity semantics only change at the moment you stop compiling it.
  • What replaces Sass mixins once the preprocessor is gone?
    Nothing wholesale. Custom properties cover value reuse — and do it better, since they are inherited and live at runtime — while `@supports`, `@media`, and utility classes cover some parameterised patterns. Repeated declaration blocks generally become a shared class you apply, or duplication you accept. Plan for this explicitly rather than discovering it mid-migration.

saying these in an interview costs you the question

  • Treating the migration as pure syntax cleanup
  • Assuming failures will surface as build errors
  • Expecting custom properties to replace mixins
  • Ignoring older engines that cannot parse nesting
  • Believing devtools will show flattened selectors

context