CSS Selector Merging: Cascade and Specificity Risks
A CSS formatter adjusts indentation, newlines, and whitespace without altering source semantics or selector order. However, CSS minifiers or optimizers may merge or reorder selectors to shrink file size, ignoring how source order resolves specificity ties. This article clarifies boundaries and demonstrates specificity and cascade checks.
1. Problem: blurred boundaries between formatters and minifiers
A formatter improves source readability while strictly preserving rule declaration order. A minifier aims to reduce byte size. When an optimization tool merges identical selectors or reorders rules, equal-specificity selectors defined later in the source may move earlier, accidentally overriding expected styling.
2. Minimal reproduction: swapped source order for equal specificity
In the following CSS, both .btn-primary and .btn-danger have a specificity of (0,1,0). When applied to the same element, the later rule takes precedence. Swapping their order during optimization alters the rendered background color.
/* Source order: .btn-danger appears last and overrides .btn-primary when both classes exist */
.btn-primary { background-color: #007bff; }
.btn-danger { background-color: #dc3545; }
/* Faulty reordered output: .btn-primary incorrectly overrides .btn-danger */
.btn-danger { background-color: #dc3545; }
.btn-primary { background-color: #007bff; }3. Root cause: synergy of cascade order and specificity
Browsers resolve CSS conflicts by: (1) origin and importance, (2) specificity (ID, Class, Type), and (3) source order. For identical specificities, source order determines the winner. Tools operating without DOM context that reorder rules break order-dependent cascade logic.
4. Recommendation: keep formatters focused and enable safe minification
Use formatters like Prettier or Stylelint strictly for layout and whitespace, turning off rule reordering. Configure minifiers like Clean-CSS or cssnano with safe modes or disable unsafe rule merging. Adopt BEM or CSS Modules to reduce reliance on global source order overrides.
5. Complete code: selector specificity calculation and reorder check
This TypeScript function calculates simple selector specificity (ID, class/attr/pseudo, type) and determines whether swapping two selectors risks breaking cascade precedence.
type Specificity = [number, number, number];
function calculateSimpleSpecificity(selector: string): Specificity {
let ids = 0;
let classes = 0;
let types = 0;
const clean = selector.trim();
const idMatches = clean.match(/#[a-zA-Z0-9_-]+/g);
if (idMatches) ids = idMatches.length;
const classMatches = clean.match(/\.[a-zA-Z0-9_-]+/g);
if (classMatches) classes = classMatches.length;
const attrMatches = clean.match(/\[[^\]]+\]/g);
if (attrMatches) classes += attrMatches.length;
const pseudoClasses = clean.match(/:[a-zA-Z0-9_-]+/g);
if (pseudoClasses) classes += pseudoClasses.length;
const typeMatches = clean
.replace(/#[a-zA-Z0-9_-]+/g, "")
.replace(/\.[a-zA-Z0-9_-]+/g, "")
.replace(/\[[^\]]+\]/g, "")
.replace(/:[a-zA-Z0-9_-]+/g, "")
.match(/[a-zA-Z0-9_-]+/g);
if (typeMatches) types = typeMatches.length;
return [ids, classes, types];
}
function compareSpecificity(a: Specificity, b: Specificity): number {
if (a[0] !== b[0]) return a[0] - b[0];
if (a[1] !== b[1]) return a[1] - b[1];
return a[2] - b[2];
}
function isSafeToReorder(sel1: string, sel2: string): boolean {
const spec1 = calculateSimpleSpecificity(sel1);
const spec2 = calculateSimpleSpecificity(sel2);
return compareSpecificity(spec1, spec2) !== 0;
}
console.log(calculateSimpleSpecificity("#nav .item.active a")); // [1, 2, 2]
console.log(isSafeToReorder(".btn-primary", ".btn-danger")); // false (both [0, 1, 0], unsafe to reorder)6. Incorrect approaches
Sorting all rules alphabetically for cosmetic reasons. Adding !important blindly to resolve order conflicts. Relying on a code formatter to perform unsafe byte-level optimization.
7. Boundaries: CSS layers (@layer), pseudo-classes, and :is() / :where()
With modern CSS, @layer precedence supersedes normal source order. The :where() pseudo-class yields (0,0,0) specificity, while :is() takes the highest specificity among its arguments. Specificity checks must incorporate these syntax features.
8. Verification
Use Stylelint to block unsafe selector reordering. Compare computed styles, cascade order, and critical-page screenshots in real browsers across responsive breakpoints and interaction states.
9. FAQ
Does a CSS formatter alter selector order? Standard formatters like Prettier modify whitespace and indentation only, never rule order.
Why do two classes with equal specificity override differently? CSS cascade rules mandate that the later rule in the stylesheet wins when specificities are equal.
When can selectors be merged safely? Only when identical selectors are contiguous or have no intersecting conflicting rules between them.
10. Summary
Keep CSS formatters and minifiers clearly separated. Formatters ensure readable code layout, while minifiers must strictly honor cascade order and specificity rules whenever merging or reordering selectors.
Sources and further reading
Specifications and references for checking the article’s technical details.
Related articles
CSS Property Reordering: PostCSS and Fallback Risks
Analyze safety boundaries when reordering CSS properties with PostCSS AST, focusing on browser fallbacks, vendor prefix order, and CSS variable hazards.
Best practicesPurgeCSS: Unused Styles, Safelists and Minification
Explain PurgeCSS unused style removal principles, prevent dynamic class name stripping, and combine Tree-shaking with Minification and CDN caching.
PitfallsCRLF vs LF: Fix Line Ending Diffs with Git
Analyze how line ending mismatches between Windows (CRLF, \r\n) and Linux/macOS (LF, \n) cause whole-file false diffs, and configure Git line ending normalization.
Keep exploring
Use the related browser tool to test the workflow with your own sample data.
Open related tool