DEV Community

Cover image for Explaining CSS Specificity Rules
DrPrime01
DrPrime01

Posted on

Explaining CSS Specificity Rules

While working on my recent article, I had a dropdown set to display: block; only when the user hovers over it. The problem is that the dropdown still shows regardless. It was not removed from the viewport despite having display: none; applied. I inspected the dropdown in the browser’s dev tools and found display: none; — it was struck through. Upon further inspection, I discovered the parent <nav> had a display: flex; declaration using the .navbar ul selector. The dropdown was a <ul> with a class of .dropdown-menu set to display: none, but the style had no effect. Alas! .navbar ul used both a class and an element selector, giving it a specificity of 0, 0, 1, 1, compared to .dropdown-menu with a specificity of 0, 0, 1, 0.

CSS, which stands for Cascading Style Sheets, is a rulebook that defines the styles of a markup language, such as HTML and XML. The “cascading” in the name refers to the set of rules the browser follows to resolve naming conflicts. It works by considering three factors, one of which is selector specificity, the focus of this article.

What is CSS Specificity?

CSS specificity is a scoring system or a tie-breaking algorithm that helps the browser determine which selector takes precedence over others. It is the most common reason for style conflicts in development. You see, when writing CSS, you can mistakenly apply more than one style declaration to a single element. It might be with a class selector, an ID selector, an element selector, or even an inline style.

<style>
p {
  color: blue;
}

.greeting {
  color: green;
}

#greeting-text {
  color: rebeccapurple;
}

</style>

<p class="greeting" id="greeting-text" style="color: red">Hello</p>
Enter fullscreen mode Exit fullscreen mode

When the browser encounters all these declarations, each targeting a single element, it uses selector specificity to determine which declaration wins and which are ignored. As you’ll see in the next section, each selector has a rank, which the browser calculates, and the selector with a higher score wins.

Specificity is just one of the cascade rules. When declarations collide, as in the scenario above, the cascade considers three factors to determine the winner: stylesheet origin, selector specificity, and source order.

Stylesheet Origin

This is the first checkpoint. You see, styles are sorted into layers of origin, which are grouped into: user-agent styles (browser’s default styles), user styles (the custom external styles you load in your app), and author styles (the ones you wrote), in order of increasing precedence. A declaration in any origin will be preferred over those lower than it, but there’s an exception — !important. When you add !important to your declaration, it attains the highest priority, so the browser ignores every other declaration and uses that declaration.

Source Order

This is reserved for the last. If two rules have the same origin and specificity, the source order is used as the final tie-breaker. It implements a simple rule: the last declaration wins. Whichever rule is defined last in the stylesheet (or loaded last if using multiple files) will be applied.

/* Specificity: 0,1,0 (one class) */
.my-box {
  color: blue;
}

/* Specificity: 0,1,0 (one class) */
.my-box {
  color: red;
}

/* The box will be red, because this rule was defined last. */
Enter fullscreen mode Exit fullscreen mode

The Four Categories Of Specificity And Their Scores

CSS has four categories of specificity, denoted by the common 0, 0, 0, 0 for scoring. Sometimes, the (A, B, C, D) notation is used, but it’s less popular than the former. In this notation, the first 0 represents inline styles.

<p style="color: red">Hello</p>
Enter fullscreen mode Exit fullscreen mode

Since they are applied directly to the element, they form a scoped declaration that overrides any declaration to the same element within the CSS file or the <style> tag. The only exception, of course, is if a declaration targeting the same element in the stylesheet has !important — in this case, it overwrites the inline style. They have a score of 1, 0, 0, 0.

Inline styles are not categorised as selector specificity. The styles are applied directly to the element via the style attribute, not within the <style> tag or a CSS file.

For simplicity, some authors restrict the score notation to the stylesheet or within the <style> tag, making it 0, 0, 0, instead of 0, 0, 0, 0. So, wherever you see the triple-digit notation, always know that the first 0 is for the ID selector.

The second 0 represents the ID selector. Within a stylesheet or the <style> tag, it has the highest specificity. It has a score of 0, 1, 0, 0, and will override every other selector, except a selector that has a declaration with !important.

#nav-menu {
  display: flex;
}
Enter fullscreen mode Exit fullscreen mode

The third 0 is for classes (.dropdown), pseudo-classes (:hover), and attribute selectors ([type="text"]). These three selectors fall into the same category and have the same score: 0, 0, 1, 0. So, what happens when you run into this issue below:

<style>
.dropdown-link {
  color: green;
}

[href="/products"] {
  color: blue;
}

/* Note: a pseudo-class cannot be used as a standalone selector, so it doesn't work in this example */
</style>

<a class="dropdown-link" href="/products">Products</a>
Enter fullscreen mode Exit fullscreen mode

Since they have the same specificity score of 0, 0, 1, 0, which declaration will be applied? What will be the color of the link tag’s text? If you choose blue, you’re absolutely right! If you remember correctly, I mentioned source order in the previous section; It comes into play in this scenario. The last declaration overwrites the previous one if both selectors have the same specificity and origin. You can see it’s a bit tricky. Once you’re familiar with the rules, there’s nothing to worry about.

The last and final 0 goes to elements and pseudo-elements, like ul, p, ::before, ::after. This least-specificity ranking is reserved for selectors that target element types. They have a score of 0, 0, 0, 1, and any other selector’s declaration will override theirs, except, of course, if they have !important.

Now that you’ve seen the inline specificity and the three other selector specificity ranks, it is also important to tell you about the few aberrations. Selectors like the universal selector (*), combinators (>, +, ~), and the :where() pseudo-class have a specificity of 0, and will never override any of your other selectors.

How To Calculate Specificity

Specificity is a common frustration for developers: you add a new CSS rule, reload the page, and... nothing happens. The style you just wrote is completely ignored, often struck through in the browser's dev tools. This problem rarely surfaces in small projects with a single stylesheet. However, as a project scales, conflicts over specificity become almost inevitable. This happens when the number of CSS files increases, you integrate third-party frameworks, or you write complex, nested selectors. It becomes difficult to predict which rule will "win" when multiple declarations target the same element, leading to unexpected and hard-to-debug style overrides. It is therefore important to know how to calculate specificity.

In the last section, you read the scores of different selectors, but it is not enough. There are situations where an element can have more than one selector of different specificity. Let’s examine them and see how they are calculated.

Class And Tag Selectors

.navbar ul {
  display: flex;
}
Enter fullscreen mode Exit fullscreen mode
  • Specificity score: 0, 0, 1, 1
  • Breakdown:
    • .navbar: 1 class (0, 0, 1, 0)
    • ul: 1 element (0, 0, 0, 1)

Tag, Class, And Attribute Selectors

input.form-control[required] {
  border-color: red;
}
Enter fullscreen mode Exit fullscreen mode
  • Specificity Score: 0, 0, 2, 1
  • Breakdown:
    • input: 1 Element (0,0,0,1)
    • .form-control: 1 Class (0,0,1,0)
    • [required]: 1 Attribute (0,0,1,0)

ID And Tag Selectors

#main-content p {
  line-height: 1.6;
}
Enter fullscreen mode Exit fullscreen mode
  • Specificity Score: 0, 1, 0, 1
  • Breakdown:
    • #main-content: 1 ID (0, 1, 0, 0)
    • p: 1 Element (0, 0, 0, 1)

Class And Pseudo-class Selectors

.nav-item:hover .dropdown-menu {
  display: block;
}
Enter fullscreen mode Exit fullscreen mode
  • Specificity Score: 0, 0, 3, 0
  • Breakdown:
    • .nav-item: 1 Class (0, 0, 1, 0)
    • :hover: 1 Pseudo-class (0, 0, 1, 0)
    • .dropdown-menu: 1 Class (0, 0, 1, 0)

Tag And Class Selectors

button.btn.btn-danger {
  background-color: darkred;
}
Enter fullscreen mode Exit fullscreen mode
  • Specificity Score: 0, 0, 2, 1
  • Breakdown:
    • button: 1 Element (0, 0, 0, 1)
    • .btn: 1 Class (0, 0, 1, 0)
    • .btn-danger: 1 Class (0, 0, 1, 0)

ID, Class, and Pseudo-element Selectors

#header .title::before {
  content: "► ";
}
Enter fullscreen mode Exit fullscreen mode
  • Specificity Score: 0, 1, 1, 1
  • Breakdown:
    • #header: 1 ID (0, 1, 0, 0)
    • .title: 1 Class (0, 0, 1, 0)
    • ::before: 1 Pseudo-element (0, 0, 0, 1)

Each selector used in the rulesets has its specificity in the correct position, and when more than one has the same specificity, their specificities are summed. You can use this CSS Specificity Calculator by Keegan Street to save you time.

To make it more interesting, let's set up the "battleground" with a single HTML element and a stylesheet in which multiple rules compete to style it.

The HTML Target

First, let's set the stage. We have one simple "Home" link. This is the element all our CSS rules will fight over.

    <div id="main-nav" class="navbar">
      <ul>
        <li class="nav-item active">
          <a href="#" class="nav-link" title="Home" > Home </a> 
        </li>
      </ul>
    </div>
Enter fullscreen mode Exit fullscreen mode

The CSS Stylesheet (The Battleground)

Here are all the competing rules, which the browser reads from top to bottom.

/* Specificity is scored (A, B, C, D) A = Inline Styles, B = IDs, C = Classes, D = Elements */ 

/* --- Rule 1 --- */ 

a { 
  color: black; 
  /* Specificity: 0,0,0,1 (1 element) */ 
} 

/* --- Rule 2: The !important modifier --- */ 

.nav-link { 
  color: darkgreen !important; 
  /* Specificity: 0,0,1,0 (1 class) */ 
} 


/* --- Rule 3 --- */ 

.navbar a { 
  color: green; 
  /* Specificity: 0,0,1,1 (1 class, 1 element) */ 
} 

/* --- Rule 4 --- */ 

li.active a { 
  color: purple; 
  /* Specificity: 0,0,1,2 (1 class, 2 elements) */ 
} 

/* --- Rule 5: The ID Jump --- */ 

#main-nav .active a { 
  color: crimson; 
  /* Specificity: 0,1,1,1 (1 ID, 1 class, 1 element) */ 
} 

/* --- Rule 6a: The Source Order Twist (Part 1) --- */ 

#main-nav li.active a { 
  color: steelblue; 
  /* Specificity: 0,1,1,2 (1 ID, 1 class, 2 elements) */ 
} 

/* --- Rule 6b: The Source Order Twist (Part 2) --- */ 

div#main-nav .active a { 
  color: goldenrod; 
  /* Specificity: 0,1,1,2 (1 ID, 1 class, 2 elements) */ 
}
Enter fullscreen mode Exit fullscreen mode

The Showdown: A Step-by-Step Analysis

The browser must decide which color to apply. It does this by following the cascade rules:

Step 1: The !important Exception

The browser's first pass isn't for specificity; it's to find any !important declarations.

  • It finds Rule 2 (.nav-link) has color: darkgreen !important;.
  • This declaration is moved to a special, high-priority "bucket." It is now the front-runner to win, unless another !important rule challenges it.
  • None of the other rules are !important, so darkgreen is the current champion.

Step 2: The "Normal" Specificity Battle

Next, the browser finds the winner among all the normal (non-important) rules. This is where specificity scores are compared.

  • Rule 1 (a) (0,0,0,1): This is our baseline.
  • Rule 3 (.navbar a) (0,0,1,1): Rule 3 beats Rule 1 because it has a class.
  • Rule 4(li.active a) (0,0,1,2): Rule 4 beats Rule 3. It has the same number of classes, but more elements (li, a) versus just (a).
  • Rule 5 (#main-nav .active a) (0,1,1,1): Rule 5 soundly beats Rule 4. As soon as an ID is introduced, it beats any number of classes. The score in the "B" column (IDs) is more important than anything in the "C" (Classes) or "D" (Elements) columns.
  • Rule 6a (#main-nav li.active a) (0,1,1,2): Rule 6a beats Rule 5. Its score is higher in the "D" column (2 > 1) while the "B" and "C" columns are identical.

Step 3: The Source Order Tie-Breaker

Now, a new challenger appears.

  • Rule 6b (div#main-nav .active a) (0,1,1,2): Look at this score! It's identical to Rule 6a's score (0,1,1,2).
  • The browser can't use specificity to decide. It moves to the final tie-breaker: Source Order.
  • Because Rule 6b is defined after (lower down in the file) Rule 6a, Rule 6b beats Rule 6a.
  • The winner of the "normal" specificity battle is Rule 6b, which wants to set the color to goldenrod.

Step 4: The Final Verdict

The browser now has its two champions:

  1. The !important winner: Rule 2 (darkgreen)
  2. The "normal" specificity winner: Rule 6b (goldenrod)

The cascade rule is clear: An !important declaration always beats a normal declaration, regardless of specificity. Therefore, Rule 2 beats Rule 6b, and the final applied color on the link will be darkgreen.

Back To The Dropdown: How Did I Solve It?

Initially, these were the rulesets that both affected the dropdown:

.navbar ul {
  display: flex;
}

.dropdown-menu {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  background: #555;
  width: 200px;
  z-index: 100;
}

.dropdown-container:hover .dropdown-menu {
  display: block;
}
Enter fullscreen mode Exit fullscreen mode

The first ruleset sets every <ul> tag that is a descendant of the .navbar container to display: flex;, yet I expected the .dropdown-menu, which is a <ul> and a descendant of .navbar, to be hidden by default using display: none;. Someone without the knowledge of specificity would have wondered what went wrong, and probably got frustrated, but that’s not the case here.

Let’s use the rule to see where my mistake is.

The .navbar ul selector combination has a class (0, 0, 1, 0) and a tag (0, 0, 0, 1), giving a combined score of 0, 0, 1, 1. On the other hand, .dropdown-menu is a single class with a score of 0, 0, 1, 0, therefore, compared to the former, it has a lower point, and the former overrides it.

So, how did I solve it?

I introduced a clean fix. I refactored the conflicting rule by introducing the direct descendant combinator (>) to .navbar ul

.navbar > ul {
  display: flex;
}

.dropdown-menu {
  display: none;
  position: absolute;
  top: 100%;
  left: 0;
  background: #555;
  width: 200px;
  z-index: 100;
}

.dropdown-container:hover .dropdown-menu {
  display: block;
}
Enter fullscreen mode Exit fullscreen mode

With this little change, only the main nav <ul>s are targeted, and the nested descendants are spared. This is a simple fix that often eliminates the problem without creating more specificity issues. However, that’s not the only way to fix it.

Increasing the specificity rank by changing .dropdown-menu to .navbar .dropdown-menu also works. This gives the dropdown a new score of 0, 0, 2, 0, which beats the former 0, 0, 1, 0 and the .navbar ul's 0, 0, 1, 1.

And if you want to brute-force it (not advisable), you can add !important behind display: none;. This overrides every other rule, and the browser uses it.

.dropdown {
  display: none !important;
}
Enter fullscreen mode Exit fullscreen mode

However, the catch is that it might disrupt other declarations, like in this case. When !important was introduced, even the display: block; that’s supposed to take effect on hover, is ignored by the browser.

.navbar > ul {
  display: flex;
}

.dropdown-menu {
  display: none !important;
  position: absolute;
  top: 100%;
  left: 0;
  background: #555;
  width: 200px;
  z-index: 100;
}

.dropdown-container:hover .dropdown-menu {
  display: block;
}
Enter fullscreen mode Exit fullscreen mode

You must be cautious when using it to avoid digging yourself into a deeper pit.

Debugging CSS Specificity Issues

You see, everything you’ve read up to this point can be achieved only if you know the root of the problem. If you can’t trace which declaration is overriding which, there’s no point in attempting to fix it.

Let’s see how to debug specificity problems using your browser’s developer tools in a step-by-step fashion.

Your browser's built-in "DevTools" (in Chrome, Firefox, Edge, or Safari) is the ultimate source of truth. You don't need any special extensions or tools to get started.

  • First, find the element on your page that isn't styling correctly.

    • Right-click on that element.
    • Select Inspect from the context menu.

    This will open the Developer Tools, with the HTML element highlighted in the "Elements" panel and, most importantly, all of its CSS rules displayed in the "Styles" panel (it's called "Rules" in Firefox).

  • For the next part, you check the Styles panel. The Styles panel is your battleground. It lists every single CSS rule that matches your selected element, in order of precedence.

    • The Winning Rule: The rule at the very top of a section is the one that is currently winning.
    • Struck-Through Styles: This is the most important clue. When you see your style (e.g., color: red;) with a line through it, it means the browser applied that rule, but it was overridden by a more specific rule.
    • Find the Culprit: Look directly above your struck-through style. You will see the exact rule that overrode yours. It will be the one that isn't struck through.
    • Hover for the Score: In modern browsers like Chrome, you can see a link with the text “specificity…bug.css:28”. It takes you to the line of your code where the issue is. This is the fastest way to confirm.
  • Use the Computed tab. This is your final source of truth. It shows a giant list of every single CSS property on the element. It only shows the final, calculated value that is actually being rendered. You can filter this list (e.g., type "color") to see the final applied color. In most browsers, you can click a small arrow or icon next to the winning property, and it will jump you back to the "Styles" panel and highlight the exact rule that won the battle.

To help you understand, let me show you a simple debugging workflow for the dropdown issue.

  1. Inspect the target element (the dropdown).

  2. Go to the "Styles" panel and find your CSS file and the rule you just wrote.

  3. Is it struck-through?

    • YES: Look above it to find the winning rule. By the right side of the declaration, you can see a link that says “specificity…bug” and where it’s located. Click it and locate the issue. You now have two choices:
      1. Make your selector more specific to beat it.
      2. Make the other selector less specific so yours can win (this is often the better, cleaner choice for maintenance).
    • NO: If your rule is at the top and not struck-through, but the style still looks wrong, you're likely editing the wrong element, or your CSS file isn't being loaded at all.

After you’ve found the source of your failing declaration, you can refer to any fix from the section above.

Conclusion

CSS, at its heart, is a predictable system. The frustration we feel when a style doesn't apply isn't magic; it's a conflict within the cascade. As we've seen, the browser follows a clear set of rules to resolve these conflicts, checking stylesheet origin first, then selector specificity, and finally source order.

More often than not, the battle is won or lost at the level of specificity. By remembering the scoring system, which we can think of as (Inline, ID, Class, Element), we can see exactly why a rule like .navbar ul might beat a seemingly direct class like .dropdown-menu. An ID beats any number of classes, a class beats any number of elements, and an inline style beats them all.

Understanding this hierarchy moves you from a developer who guesses why CSS isn't working to one who knows why. That stuck, overridden style is no longer a mystery but a puzzle you have the key to solve. So, the next time a style doesn't apply, don't reach for !important. Instead, open your browser's dev tools, inspect the element, and look at the "Styles" panel. It will show you the competing rules and which one won. By favouring simpler, lower-specificity selectors and understanding how to read the cascade, you'll write CSS that is not only more powerful but far easier to debug and maintain.

References

Top comments (0)