CSS Cascade and Specificity

In web development, CSS specificity is the critical mechanism that determines which style a browser renders when multiple rules attempt to set the same property on an element. Browsers calculate a priority value for each rule based on a specific algorithm to determine the final applied style. Mastering this concept is essential for building maintainable and logically clear code, effectively avoiding debugging challenges caused by "style conflicts."

CSS specificity calculation follows a strict mathematical model defined by the W3C standard. Its core principle is that priority calculation does not consider the order in which CSS rules appear in files (i.e., later definitions do not necessarily override earlier ones); instead, it relies entirely on the structural complexity of the selectors. To facilitate understanding and application, the industry typically simplifies priority into three dimensions of weight values for cumulative calculation.

Three Dimensions of Weight

The weight of a CSS selector is composed of the following three parts, ranked from highest to lowest importance:

  1. ID Selectors: Selectors in #id form have the highest weight. Each ID selector contributes 100 points.
  2. Class, Attribute, or Pseudo-class Selectors: Including .class, [attr], and pseudo-classes like :hover. Each such selector contributes 10 points.
  3. Tag Name and Pseudo-element Selectors: Such as div or ::before. Each such selector contributes 1 point.

It is worth noting that the weight of a compound selector is the product sum of its parts' weights. For example, a compound selector containing one ID and one class (#id .class) has a priority of $100 + 10 = 110$; whereas a selector combining two classes (.class1 .class2) results in $10 \times 10 = 100$.

Priority Calculation Example Analysis

To understand this more intuitively, we can verify the above rules by comparing the weights of different selectors:

  • Scenario One:

    • Rule A: .btn (Weight: 10)
    • Rule B: div.btn (Weight: $1 + 10 = 11$)
    • Result: Rule B wins. Even if div.btn is written at the end of the file, it still overrides the .btn style because its weight is higher.
  • Scenario Two:

    • Rule A: #header .nav (Weight: $100 + 10 = 110$)
    • Rule B: .nav (Weight: 10)
    • Result: Rule A wins. The high weight of the ID selector allows it to easily override ordinary class selectors unless countered by a higher-weight ID.
  • Scenario Three (Special Case):

    • Rule A: ul > li (Weight: $1 \times 1 = 1$)
    • Rule B: ul li (Weight: $1 \times 1 = 1$)
    • Result: Weights are equal. In this case, the later-defined rule overrides the earlier one. This reminds us that file order becomes critical when weights are identical.

Best Practices for Improving Maintainability

In real-world projects, blindly using high-weight selectors (such as excessive IDs) often leads to code that is difficult to maintain. While ID selectors have extremely high priority, they are typically used for unique elements in a page (like <html> or <body>); abusing them increases the risk of conflicts. Here are several practical suggestions:

  • Prioritize Class Selectors: For most component and styling requirements, class selectors (.class) are best practice. They offer sufficient flexibility with moderate weights, making accidental overrides less likely.
  • Use ID Selectors Cautiously: Use IDs only when you must precisely target a unique element. Avoid defining the same ID repeatedly within loops to prevent logical confusion.
  • Leverage BEM Naming Conventions: Adopting the Block (block), Element (element), Modifier (modifier) naming pattern (e.g., .button--primary) allows for style isolation and combination without increasing weight, reducing conflicts at the source.
  • Understand Inheritance and Overrides: Understanding CSS inheritance characteristics helps optimize selector structures. Sometimes adjusting the parent element's styles resolves issues without needing to create complex nested selectors for child elements.

Summary

CSS priority mechanisms are indispensable foundational knowledge in frontend development. They provide an objective, quantitative standard for resolving style conflicts. Developers should memorize the weight ranking "ID > Class/Attribute/Pseudo-class > Tag/Pseudo-element" and follow the principle of minimal sufficiency when writing code, avoiding over-reliance on high-weight selectors. By reasonably applying this mechanism, we can build more robust, stable, and collaborative frontend applications.

// Example demonstrating specificity calculation logic
function calculateSpecificity(selector) {
    const idCount = (selector.match(/#[a-zA-Z_][\w-]*/g) || []).length;
    const classOrAttrCount = (selector.match(/(?:\.|\[)[^\s>]+/g) || []).length;
    const tagOrPseudoElementCount = selector.trim().split(/\s+/).filter(token => {
        return !token.startsWith('#') && 
               !token.includes('.') && 
               !token.includes('[');
    }).length;

    // Specificity Formula: (idCount * 100) + (classOrAttrCount * 10) + tagOrPseudoElementCount
    const specificity = (idCount * 100) + (classOrAttrCount * 10) + tagOrPseudoElementCount;

    return { idCount, classOrAttrCount, tagOrPseudoElementCount, total: specificity };
}

// Test cases
console.log(calculateSpecificity('.btn')); 
// Output: { idCount: 0, classOrAttrCount: 1, tagOrPseudoElementCount: 0, total: 10 }

console.log(calculateSpecificity('div.btn')); 
// Output: { idCount: 0, classOrAttrCount: 1, tagOrPseudoElementCount: 1, total: 11 }

console.log(calculateSpecificity('#header .nav')); 
// Output: { idCount: 1, classOrAttrCount: 1, tagOrPseudoElementCount: 0, total: 110 }