Javascript is required
PitfallsPublished 2026-07-28Updated 2026-08-087 min read

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.

By: Tiny
CSS FormatterSpecificityCascadeSelector Merge

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

Keep exploring

Use the related browser tool to test the workflow with your own sample data.

Open related tool