Reported: March 31, 2026 — stylelint 16.5.0
Stylelint’s recent 16.5 release rewrites how the no-descending-specificity rule treats nested selectors — and the first time a mid-sized design system runs it, you can expect a pile of warnings. The headline number, 1,284, is the kind of count a Sass codebase with several hundred components tends to produce when the rule finally resolves correctly against CSS nesting. The right fix is almost never disabling the rule; it’s reordering selectors.
What changed in the no-descending-specificity rule in 16.5?
Version 16.5 shipped two fixes that matter for anyone running no-descending-specificity against modern CSS. The first fix corrects false positives for nested selectors introduced by PR #7724, which taught the rule to treat the & nesting selector’s specificity as the largest specificity among the parent’s selector list — the behaviour defined in the CSS Nesting specification. The second fix, from PR #7701, corrects the end positions reported by the rule so LSP integrations and editor squiggles line up with the actual selector range.
The practical effect: before 16.5, the rule either missed violations that existed inside nested rules or reported them against the wrong character range. After 16.5, it catches them and points at the right token. If you maintain a Sass codebase or use native CSS nesting in the browser, the rule just got substantially more accurate, which is why upgrade diffs surface so many new warnings.

Straight from the source.
The official documentation page for the rule spells out the contract in plain language: disallow selectors of lower specificity from coming after overriding selectors of higher specificity. The screenshot above captures the rule’s option block, including the ignore: ["selectors-within-list"] and severity flags that most teams rediscover after their first upgrade.
Why did upgrading to 16.5 surface 1,284 new warnings?
Large error counts on upgrade come from one source: pre-16.5 versions of the rule underreported inside nested blocks. A Sass partial like the one below looks innocent when you read it top-to-bottom, but it violates the rule’s contract — and 16.5 is the first release that flags it reliably.
.card {
a {
color: var(--link);
}
&.is-featured a {
color: var(--link-featured);
}
a:hover {
color: var(--link-hover);
}
}
The compiled selectors come out as .card a, then .card.is-featured a, then .card a:hover. Specificity on the middle rule is 0,2,1; the trailing .card a:hover is 0,1,2. Lower specificity appearing after higher specificity — a textbook violation of the rule. Older Stylelint releases missed cases like this because the nesting selector’s effective specificity wasn’t computed correctly. 16.5 fixes that.
Multiply this pattern across a component library with 400 partials and the 1,284 figure stops looking surprising. None of the code is newly wrong — the linter just sees it now, because the nesting-selector specificity calculation finally matches what the CSS Nesting spec describes.

Runtime on large codebases is the practical concern once violations are fixed. The chart above plots scan time for the no-descending-specificity rule across codebase sizes: the rule’s per-selector work is cheap on small projects but climbs noticeably once a file exceeds a few thousand selectors, because each new selector is compared against every prior selector sharing a base. Teams with monolithic stylesheets feel this; teams with one small file per component barely notice it.
Stylelint 16.5 vs. Biome’s no-descending-specificity — which fits your stack?
Biome shipped its own noDescendingSpecificity lint rule in 2024 as part of its CSS linter, and the rule is a near port of Stylelint’s. If you’re picking between them, three axes decide the answer: plugin ecosystem, Sass support, and run speed.
Plugin ecosystem. Stylelint’s rule set is larger and its plugin catalog — stylelint-scss, stylelint-order, stylelint-declaration-block-no-ignored-properties — has no Biome equivalent. If you need selector ordering, Sass-specific rules, or property-block enforcement, Stylelint is the only option. The rule’s README on GitHub documents options like ignore: ["selectors-within-list"] that Biome’s implementation doesn’t currently expose.
Sass support. Stylelint supports custom syntaxes, which means the rule can run against source Sass rather than compiled CSS. Biome parses CSS only. For a codebase written in Sass, Stylelint 16.5 catches specificity problems in the file a developer is editing; Biome requires you to run it against the compiled output, which loses the ability to point at the original source line.
Run speed. Biome’s CSS linter, written in Rust, is faster per-file than Stylelint’s Node.js implementation — often by an order of magnitude on cold runs. Biome’s rule documentation describes a simpler option surface and a single-pass analyzer. If your codebase is plain CSS and you value whole-repo scans completing in under a second, Biome wins on speed. If you care about catching violations in source Sass with the 16.5 nesting fix, Stylelint wins on correctness.

Purpose-built diagram for this article — Stylelint 16.5: The no-descending-specificity Rule That Caught 1,284 Errors.
The diagram above maps the decision: Sass source, or heavy reliance on Stylelint plugins, points at Stylelint 16.5; plain-CSS codebases where scan time dominates CI cost point at Biome. Most teams end up running both — Stylelint in the editor and pre-commit, Biome in CI for the speed.
How should you remediate the violations the 16.5 upgrade surfaces?
There are four realistic responses when your CI pipeline suddenly reports 1,284 new errors against stylelint 16.5 no-descending-specificity. In rough order of what I’d recommend:
- Reorder selectors at the source. The fix the rule wants is for higher-specificity selectors to come after lower-specificity ones. For most violations, moving the hover or state rule below the base rule is a one-line change.
- Enable the
ignoreoptions where the rule is wrong for your style. Theignore: ["selectors-within-list"]option skips violations inside a comma-separated selector list — a common source of noise on pseudo-element sets like::-webkit-input-placeholder, ::-moz-placeholder. - Demote the rule to a warning, not off. In
.stylelintrc, set"severity": "warning"on the rule while you work through the backlog. Your CI stays green, but developers see every new violation they introduce. - Disable the rule per file with an inline comment. For a legacy file you’re not touching,
/* stylelint-disable no-descending-specificity */at the top is acceptable. Disabling it globally is the worst option — you lose the signal everywhere to silence it in a few files.
The most common mechanical fix is moving state rules after base rules. This refactor:
.nav-link { color: var(--fg); }
.nav-link:hover { color: var(--fg-hover); }
.nav-primary .nav-link { color: var(--fg-primary); }
…should become:
.nav-link { color: var(--fg); }
.nav-primary .nav-link { color: var(--fg-primary); }
.nav-link:hover { color: var(--fg-hover); }
The rendered result is identical as long as no other rule depends on the source order — the rule is forcing you to match source order to specificity, which is the whole design goal.
For Sass files, the fix usually means hoisting &:hover and similar state rules to the bottom of the parent block. A batch codemod can do this — PostCSS with a small walker script will reorder declarations inside a rule by computed specificity in under 50 lines.
A caution on --fix: the no-descending-specificity rule does not ship with an autofixer, and for good reason. Reordering selectors can change the cascade if two selectors of equal specificity target the same property — the later one wins, and a codemod that reorders them silently flips which value applies. The official rule page explicitly notes the linter doesn’t inspect declarations, which is why automatic fixing would be dangerous.

The Reddit threads captured above run through the same conversation the Stylelint maintainers have had on GitHub for years: is this rule worth the noise? The consensus, and it’s the right one, is yes — but only if you’re willing to treat specificity order as a real stylistic convention rather than a rubber-stamp. Teams that treat the rule as optional noise tend to disable it and silently regress; teams that treat violations as bugs tend to end up with cleaner cascades and less hover-state flicker.
What the rule doesn’t catch
Three limits to keep in mind. The rule doesn’t read HTML, so it can’t tell whether a selector ever matches an element — it’s a structural check, not a behavioural one. It ignores !important, so an overridden !important declaration can still pass the rule while breaking the cascade in production. And it only looks at selectors, not declarations, so two selectors of descending specificity setting different properties don’t actually conflict but still trigger a warning. Treat the rule as a strong hint, not proof of a bug.
Pinning the upgrade safely
If you’re not ready for 1,284 warnings this week, pin to the previous minor release and open a tracking issue. The 16.5 nesting fix is additive — no existing behaviour silently changed in the other direction — so pinning is a safe holding pattern until your team has time to work through the list. When you do upgrade, add --max-warnings 0 to your lint script to prevent new violations from sneaking in while the backlog shrinks.
The bigger point: no-descending-specificity is one of Stylelint’s most opinionated rules, and the 16.5 nesting correction is the first time it works the way the documentation has always said it does. If you care about your cascade, spend the afternoon on the reordering pass. If you don’t, you’d never have turned the rule on in the first place.



