Have you ever written CSS and been confused by why your style didn't seem to work? You set a colour for a paragraph, but something else overrode it. You added a class, but the result was still wrong. This happens because of a concept called CSS Specificity.
In this lesson you will learn exactly what specificity is, why it exists, how the browser scores and compares selectors, and how to control which style wins every single time. By the end, you will never be surprised by a style conflict again.
π‘ Real-world connection: In professional front-end development, understanding specificity is essential for debugging layout issues, maintaining large stylesheets, working with CSS frameworks like Bootstrap, and writing clean, predictable code.
Before diving in, let's make sure you understand the building blocks used throughout this lesson.
What Is a CSS Selector?
A selector is the part of a CSS rule that identifies which HTML element(s) you want to style.
/* The selector is "p" β it targets all <p> elements */
p {
color: red;
}The Three Main Selector Types You Need to Know
1. Element selector Β· targets HTML tags by name.
p { color: red; } /* styles every <p> */
h1 { color: blue; } /* styles every <h1> */2. Class selector Β· targets elements that have a specific class attribute. Written with a dot (.) before the name.
.highlight { color: yellow; }
/* This styles any element with class="highlight" */3. ID selector Β· targets ONE specific element with a matching id attribute. Written with a hash (#) before the name.
#main-title { color: green; }
/* This styles the element with id="main-title" */π‘ Key rule to remember: An
idmust be unique on a page Β· only one element should have any givenid. Classes can be shared by many elements.
What Is a Style Declaration?
A declaration is the property: value pair inside curly braces.
p {
color: blue; /* This is one declaration */
font-size: 18px; /* This is another declaration */
}What Is Specificity?
Specificity is the scoring system that browsers use to decide which CSS rule "wins" when two or more rules target the same element and set the same property.
Think of it like a competition between your CSS rules. Every selector has a score (called its specificity weight). When two rules conflict, the one with the higher score wins, and its style gets applied.
Why Does Specificity Exist?
Imagine you're building a website. You have a general rule that makes all paragraphs grey. Then later you want one special paragraph to be red. Without specificity, the browser wouldn't know which rule to use. Specificity is the tiebreaker.
Analogy: Think of specificity like priority lanes at an airport. Economy passengers (element selectors) use the regular queue. Business class passengers (class selectors) get a faster lane. First-class passengers (ID selectors) go straight through. The more specific you are, the faster you get to the front.
A First Look at the Problem Specificity Solves
<!-- HTML -->
<p class="test" id="demo">Hello World!</p>/* Three rules, all targeting the same paragraph */
p { color: red; }
.test { color: green; }
#demo { color: blue; }Expected Output: The text "Hello World!" appears in blue.
Why blue? Because #demo (an ID selector) has a higher specificity score than .test (a class selector), which has a higher score than p (an element selector). The browser picks blue.
π€ Thinking prompt: What would happen if you removed
#demo { color: blue; }? Which colour would win then? Think about it before reading on.
CSS organises selectors into four levels, from lowest to highest priority.
Level 1 Β· Universal Selector and :where()
Weight: 0-0-0 (no priority)
The universal selector * matches everything, but it has zero specificity. The :where() pseudo-class also carries zero specificity weight.
* { color: grey; } /* weight: 0-0-0 */
:where(p) { color: grey; } /* weight: 0-0-0 */These are always overridden by any other selector.
Level 2 Β· Element Selectors and Pseudo-elements
Weight: 0-0-1
Element selectors target HTML tags. Pseudo-elements like ::before and ::after also sit at this level.
p { color: red; } /* weight: 0-0-1 */
h1 { color: blue; } /* weight: 0-0-1 */
::before { content: "β"; } /* weight: 0-0-1 */Level 3 Β· Class Selectors, Attribute Selectors, and Pseudo-classes
Weight: 0-1-0
This level includes classes (.myclass), attribute selectors ([type="text"]), and pseudo-classes (:hover, :focus, :nth-child()).
.test { color: green; } /* weight: 0-1-0 */
[type="text"] { color: green; } /* weight: 0-1-0 */
:hover { color: green; } /* weight: 0-1-0 */Level 4 Β· ID Selectors
Weight: 1-0-0
ID selectors are the highest specificity you can normally use in a stylesheet. They always override classes and element selectors.
#demo { color: blue; } /* weight: 1-0-0 */Level 5 Β· Inline Styles
Weight: Even higher than IDs
Inline styles are written directly on an HTML element using the style attribute. They override all stylesheet rules.
<p style="color: pink;">This is pink no matter what.</p>An inline style beats every other type of CSS selector.
Specificity is measured using a three-number notation: X-Y-Z
| Position | Counts... | Example |
|---|---|---|
| X (first number) | Number of ID selectors | #header β X = 1 |
| Y (second number) | Number of class, attribute, and pseudo-class selectors | .nav, [href], :hover β Y = 1 each |
| Z (third number) | Number of element and pseudo-element selectors | p, h1, ::before β Z = 1 each |
To compare two selectors, start from the left (X). The selector with the bigger X wins. If X is equal, compare Y. If Y is equal, compare Z. If all three are equal, the rule that appears later in the stylesheet wins (this is the cascade rule you may have learned previously).
Quick Weight Examples
| Selector | X (IDs) | Y (Classes) | Z (Elements) | Notation |
|---|---|---|---|---|
p | 0 | 0 | 1 | 0-0-1 |
.test | 0 | 1 | 0 | 0-1-0 |
p.test | 0 | 1 | 1 | 0-1-1 |
#demo | 1 | 0 | 0 | 1-0-0 |
p#demo | 1 | 0 | 1 | 1-0-1 |
#demo.test | 1 | 1 | 0 | 1-1-0 |
Example 1 Β· Element Selector Alone
<html>
<head>
<style>
p { color: red; }
</style>
</head>
<body>
<p>Hello World!</p>
</body>
</html>Expected Output: The text "Hello World!" appears in red.
Line-by-line explanation:
pΒ· This is an element selector targeting all<p>tags.{ color: red; }Β· This sets the text colour to red.- Specificity weight: 0-0-1
- Since there is only one rule, it wins by default.
Example 2 Β· Class Selector Overrides Element Selector
<html>
<head>
<style>
.test { color: green; } /* weight: 0-1-0 */
p { color: red; } /* weight: 0-0-1 */
</style>
</head>
<body>
<p class="test">Hello World!</p>
</body>
</html>Expected Output: The text "Hello World!" appears in green.
Why green?
- Both rules target the same
<p>element. .testhas weight 0-1-0.phas weight 0-0-1.- Comparing: 0-1-0 is greater than 0-0-1 (Y column: 1 > 0).
- Green wins.
π€ Thinking prompt: What if you swap the order of the two rules in the stylesheet? Does the result change? Try it.
Example 3 Β· ID Selector Overrides Everything
<html>
<head>
<style>
#demo { color: blue; } /* weight: 1-0-0 */
.test { color: green; } /* weight: 0-1-0 */
p { color: red; } /* weight: 0-0-1 */
</style>
</head>
<body>
<p id="demo" class="test">Hello World!</p>
</body>
</html>Expected Output: The text "Hello World!" appears in blue.
Why blue?
- The
<p>element matches all three selectors. #demohas the highest weight: 1-0-0.- Blue wins, regardless of order in the stylesheet.
Example 4 Β· Combined Selector with Higher Weight
Now here is where it gets interesting. What if we combine selectors? When you write p#demo, you are selecting a <p> element that also has id="demo". This combination adds up the weights.
#demo { color: blue; } /* weight: 1-0-0 */
p#demo { color: orange; } /* weight: 1-0-1 β WINS */
.test { color: green; } /* weight: 0-1-0 */
p.test { color: yellow; } /* weight: 0-1-1 */
p { color: red; } /* weight: 0-0-1 */<p id="demo" class="test">Hello World!</p>Expected Output: The text "Hello World!" appears in orange.
Why orange?
p#demohas weight 1-0-1 (1 ID + 1 element).#demoalone has weight 1-0-0 (1 ID, no elements).- Comparing X: both have X=1. Tie. Compare Z:
p#demohas Z=1,#demohas Z=0. - 1-0-1 > 1-0-0 β orange wins.
π€ Thinking prompt: Why does
p.test(weight 0-1-1) lose to#demo(weight 1-0-0)? Because the X column (IDs) always overrules the Y and Z columns no matter how many classes or elements you have.
Example 5 Β· Equal Specificity: Last Rule Wins
When two selectors have identical weight, the browser uses the one that appears later in the stylesheet. This is called the cascade.
p { color: red; } /* weight: 0-0-1 */
p { color: blue; } /* weight: 0-0-1 β WINS because it comes last */Expected Output: Blue.
Here is the complete picture from highest to lowest priority:
| Rank | Type | Example | Weight |
|---|---|---|---|
| 1st (highest) | Inline style | <h1 style="color:pink"> | Higher than all stylesheet selectors |
| 2nd | ID selector | #navbar | 1-0-0 |
| 3rd | Class / Attribute / Pseudo-class | .test, [href], :hover | 0-1-0 |
| 4th | Element / Pseudo-element | h1, p, ::before | 0-0-1 |
| 5th (lowest) | Universal / :where() | *, :where(p) | 0-0-0 |
Let's practice reading and calculating weights of more complex selectors.
Worked Calculation 1
div p.highlight { color: purple; }Break it down:
divβ element β adds 0-0-1pβ element β adds 0-0-1.highlightβ class β adds 0-1-0- Total: 0-1-2
Worked Calculation 2
#header nav ul li a:hover { color: orange; }Break it down:
#headerβ ID β adds 1-0-0navβ element β adds 0-0-1ulβ element β adds 0-0-1liβ element β adds 0-0-1aβ element β adds 0-0-1:hoverβ pseudo-class β adds 0-1-0- Total: 1-1-4
Worked Calculation 3
.sidebar .widget h2 { color: teal; }Break it down:
.sidebarβ class β adds 0-1-0.widgetβ class β adds 0-1-0h2β element β adds 0-0-1- Total: 0-2-1
Exercise 1 Β· Predict the Winning Colour
Objective: Read the CSS and HTML below, calculate which colour wins, and explain why.
HTML:
<p id="intro" class="lead">Welcome to my website.</p>CSS:
p { color: black; } /* Rule A */
.lead { color: navy; } /* Rule B */
#intro { color: crimson; } /* Rule C */
p.lead { color: teal; } /* Rule D */Steps:
- Calculate the weight of each rule.
- Identify which rule has the highest weight.
- State which colour the text will appear.
Weights:
- Rule A:
pβ 0-0-1 - Rule B:
.leadβ 0-1-0 - Rule C:
#introβ 1-0-0 - Rule D:
p.leadβ 0-1-1
Expected Output: Text appears in crimson.
Why? Rule C (#intro) has the highest weight of 1-0-0. The X column (ID count) immediately beats all other rules regardless of their Y or Z values.
Self-check questions:
- Does changing the order of the CSS rules change the result? (No Β· specificity takes priority over order when weights are different.)
- What would happen if you removed
#introfrom the HTML element? Which colour would win then?
Exercise 2 Β· Rank These Selectors from Weakest to Strongest
Given the following selectors, rank them from lowest to highest specificity:
a) *
b) li
c) ul li
d) .menu
e) .menu li
f) #nav
g) #nav li
h) #nav .menu liAnswers with weights:
| Selector | Weight | Rank |
|---|---|---|
* | 0-0-0 | Weakest |
li | 0-0-1 | 2nd |
ul li | 0-0-2 | 3rd |
.menu | 0-1-0 | 4th |
.menu li | 0-1-1 | 5th |
#nav | 1-0-0 | 6th |
#nav li | 1-0-1 | 7th |
#nav .menu li | 1-1-1 | Strongest |
π€ Thinking prompt: Notice that
ul li(0-0-2) is stronger than.menu(0-1-0)? Wait⦠actually that's wrong! Let's check: 0-0-2 vs 0-1-0. Compare Y column: 0 vs 1..menuwins. Always compare left to right!
Exercise 3 Β· Fix the Specificity Problem
Scenario: A web developer is building a navigation bar. She expects the active link to appear orange, but it keeps appearing blue. Help her fix the issue without changing the HTML.
HTML:
<nav id="main-nav">
<a href="#" class="active">Home</a>
</nav>CSS (broken):
#main-nav a { color: blue; } /* weight: 1-0-1 */
.active { color: orange; } /* weight: 0-1-0 */Problem: .active (0-1-0) loses to #main-nav a (1-0-1) because 1-0-1 > 0-1-0.
Fix option 1 Β· increase the specificity of the orange rule:
#main-nav a { color: blue; } /* weight: 1-0-1 */
#main-nav .active { color: orange; } /* weight: 1-1-0 β WINS */Now #main-nav .active has weight 1-1-0, which beats #main-nav a at 1-0-1 because the Y column (1 vs 0) tips the balance when X is tied.
Fix option 2 Β· even more specific:
#main-nav a.active { color: orange; } /* weight: 1-1-1 β also wins */Expected Output (after fix): The "Home" link appears in orange.
Mistake 1 Β· Thinking Order Always Matters
Wrong thinking: "My rule comes after the other one, so it should win."
Correction: Order only matters when specificity weights are equal. If they are different, the higher weight always wins regardless of order.
#demo { color: blue; } /* weight: 1-0-0 */
p { color: red; } /* weight: 0-0-1 β comes after but LOSES */Output: Blue. The ID beats the element selector no matter the order.
Mistake 2 Β· Adding More Classes Does Not Beat an ID
Wrong thinking: "I have three classes on my selector, so it must beat an ID."
.a.b.c { color: green; } /* weight: 0-3-0 */
#demo { color: blue; } /* weight: 1-0-0 */Correction: No number of class selectors can ever beat a single ID selector. The X column (ID count) is always more powerful than the Y column (class count). 0-3-0 loses to 1-0-0.
Mistake 3 Β· Forgetting That Combined Selectors Add Up
Wrong thinking: "p#demo is the same as #demo."
#demo { color: blue; } /* weight: 1-0-0 */
p#demo { color: orange; } /* weight: 1-0-1 β WINS */Correction: p#demo adds the weight of both p (an element) and #demo (an ID), giving a total of 1-0-1, which is more than #demo alone at 1-0-0.
Mistake 4 Β· Thinking Inline Styles Can Be Overridden Without !important
<p style="color: purple;">I am always purple.</p>#demo { color: blue; } /* Cannot override the inline style */Correction: Inline styles override all stylesheet selectors. The only way to beat an inline style in your CSS is to use !important (which is covered in the next lesson).
Mistake 5 Β· Confusing :where() and Regular Pseudo-classes
Regular pseudo-classes like :hover or :focus have weight 0-1-0. But the special :where() function has weight 0-0-0 Β· it intentionally carries no specificity so it can be easily overridden. These are not the same.
p:hover { color: blue; } /* weight: 0-1-1 */
:where(p) { color: red; } /* weight: 0-0-0 β always loses */In this project you will build a small webpage that demonstrates all four levels of specificity working together. Each level will change the appearance of a block of text in a controlled, predictable way.
Stage 1 Β· Setup
Create your HTML structure:
<!DOCTYPE html>
<html>
<head>
<title>Specificity Demo</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="page-wrapper" class="container">
<h1>Welcome to My Page</h1>
<p>Default paragraph β styled by element selector.</p>
<p class="featured">Featured paragraph β styled by class selector.</p>
<p id="hero-text" class="featured">Hero paragraph β styled by ID selector.</p>
<p style="color: purple; font-weight: bold;">
Inline-styled paragraph β cannot be overridden by the stylesheet.
</p>
</div>
</body>
</html>