What Is CSS Specificity
CSS specificity is the “strength” of a selector when multiple rules target the same element and the browser has to pick a winner. Think of it like a points system for selectors.
Specificity only matters when competing rules have a conflict on the same property (like two different
color values).
If rules set different properties, they can happily coexist.
Also important: specificity is not the whole story. The browser decides the winner using the cascade, which
includes:
where the CSS came from, whether !important is involved, specificity, and then source order.
.card .title {
color: rebeccapurple;
}
.title {
color: seagreen;
}
p {
color: tomato;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
font-family: system-ui, Arial, sans-serif;
padding: 18px;
max-width: 980px;
}
.card {
border: 3px solid #111;
border-radius: 14px;
padding: 14px;
background: #fff;
display: grid;
gap: 8px;
}
.badge {
display: inline-block;
border: 2px solid #111;
border-radius: 999px;
padding: 4px 10px;
font-size: 13px;
width: fit-content;
background: #f2f2f2;
}
Same element, same property: who wins?I am a paragraph with class "title" inside ".card".
In the playground above, all three selectors match the paragraph, and all three set color.
The browser compares specificity and picks the strongest selector.
Specificity and the Cascade: The “Who Wins?” Checklist
When two rules fight over the same property on the same element, the browser roughly follows this order:
-
Importance and origin:
!importantrules beat non-important rules (with a few nuances). Also, user-agent, user, and author styles have an order. - Specificity: the selector with the higher specificity wins.
- Source order: if specificity ties, the rule written later wins.
In everyday “author CSS” (your stylesheet), you’ll mostly see battles decided by specificity, and then by source order.
.note {
background: #fff6cc;
border-color: #111;
}
.note {
background: #d6f5ff;
border-color: #515151;
}
.note {
background: #ffe1ee;
border-color: #999;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
font-family: system-ui, Arial, sans-serif;
padding: 18px;
max-width: 980px;
}
.note {
border: 3px solid #111;
border-radius: 14px;
padding: 14px;
display: grid;
gap: 10px;
}
.note strong {
font-weight: 700;
}
All three snippets use the same selector (
.note). That means specificity is a tie, so the last declared rule wins.
The selector doesn’t change, so the only thing changing is source order. That’s why “later wins” when specificity ties.
Learn more about the CSS Cascade in the CSS Cascade Interactive Tutorial.
CSS Specificity Rules: The Scoring System
The classic way to calculate specificity is a 3-part score:
-
A = number of ID selectors (like
#header) -
B = number of class selectors, attribute selectors, and
pseudo-classes
(like
.button,[type="email"],:hover) -
C = number of type selectors and pseudo-elements
(like
button,h2,::before)
Specificity is compared like a three-digit number: first A, then B, then C.
So (1,0,0) beats (0,99,99) because A wins first.
How to Calculate Specificity Step by Step
- Start at (0,0,0).
- Add 1 to A for each
#id. - Add 1 to B for each
.class,[attr], or:pseudo-class. - Add 1 to C for each element name (like
p) and each::pseudo-element. - Ignore combinators like
>,+,~, and whitespace. They don’t add points. -
Some functional pseudo-classes have special rules (
:is(),:where(),:not(),:has()). We’ll cover them later.
/* Specificity: (0,0,1) */
p {
border-style: dashed;
}
/* Specificity: (0,1,1) */
.notice p {
border-style: solid;
}
/* Specificity: (0,2,1) */
.notice p.warning {
border-style: double;
}
/* Specificity: (1,1,1) */
#main .notice p {
border-style: groove;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
font-family: system-ui, Arial, sans-serif;
padding: 18px;
max-width: 980px;
}
.panel {
border: 3px solid #111;
border-radius: 14px;
padding: 14px;
display: grid;
gap: 10px;
background: #fff;
}
.notice {
padding: 12px;
border-radius: 12px;
background: #f2f2f2;
}
p {
margin: 0;
padding: 12px;
border: 3px solid #111;
border-radius: 12px;
background: #fff;
}
Watch my border style change as specificity increases.
Notice how adding a class increases the middle number (B), while adding an ID increases the first number (A). IDs are specificity heavyweights.
CSS Specificity Quick Cheat Sheet
- Inline style (like
style="color:red"in the HTML) beats any selector in author CSS. #idbeats.classbeatselement.-
B bucket includes: classes
.x, attributes[x], pseudo-classes:hover,:focus,:nth-child(), etc. - C bucket includes: elements
div,pand pseudo-elements::before. -
Combinators (
>,+,~, space) don’t add specificity. -
:where()adds zero specificity no matter what’s inside. -
:is(),:not(), and:has()take the specificity of their most specific argument.
Common Selector Scores
button= (0,0,1).button= (0,1,0)#cta= (1,0,0)button.button= (0,1,1).card .button= (0,2,0)ul li a= (0,0,3)a:hover= (0,1,1).menu a::before= (0,1,2)
Specificity and Cascade in Real-World Battles
A very common “why won’t my CSS apply?” moment is: you style something with a simple selector, but another rule with a stronger selector overrides it.
Class vs Element
A class selector .highlight (0,1,0) will beat an element selector p (0,0,1).
/* (0,2,0) */
.article .highlight {
background: #ffe1ee;
}
/* (0,1,0) */
.highlight {
background: #d6f5ff;
}
/* (0,0,1) */
p {
background: #fff6cc;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
font-family: system-ui, Arial, sans-serif;
padding: 18px;
max-width: 980px;
}
.article {
border: 3px solid #111;
border-radius: 14px;
padding: 14px;
background: #fff;
}
p {
margin: 0;
border: 2px solid #111;
border-radius: 12px;
padding: 12px;
}
I am a paragraph with a class.
The ID Override
IDs are so strong that using them in CSS often makes your styles harder to override later. This is one reason many teams avoid styling with IDs.
/* (0,2,0) */
.card .title {
color: seagreen;
}
/* (1,0,0) */
#promo {
color: rebeccapurple;
}
/* (1,1,0) */
#promo.title {
color: tomato;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
font-family: system-ui, Arial, sans-serif;
padding: 18px;
max-width: 980px;
}
.card {
border: 3px solid #111;
border-radius: 14px;
padding: 14px;
background: #fff;
display: grid;
gap: 8px;
}
.title {
margin: 0;
padding: 10px 12px;
border-radius: 12px;
border: 2px solid #111;
background: #f2f2f2;
}
I have an ID and a class. IDs bring the muscle.
Compare .card .title (0,2,0) to #promo (1,0,0).
Even though .card .title has “more stuff”, it has zero IDs, so it loses.
Pseudo-Classes vs Pseudo-Elements
These look similar, but they score differently:
- Pseudo-classes use one colon and count in B:
:hover,:focus,:nth-child(),:not(). - Pseudo-elements use two colons and count in C:
::before,::after,::first-line.
/* (0,0,1) */
a {
text-decoration: underline;
}
/* (0,1,1) : pseudo-class adds to B */
a:hover {
text-decoration: none;
}
/* (0,0,2) :: pseudo-element adds to C */
a::after {
content: " ↗";
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
font-family: system-ui, Arial, sans-serif;
padding: 18px;
max-width: 980px;
}
.box {
border: 3px solid #111;
border-radius: 14px;
padding: 14px;
background: #fff;
}
a {
color: #111;
font-weight: 650;
}
Hover me and notice the selector types:
:hovervs::after.
The key lesson: pseudo-classes behave like classes for specificity, pseudo-elements behave like elements.
Learn more about pseudo-elements in the CSS ::before and ::after Pseudo-Elements Interactive Tutorial.
Special Specificity Rules: :is(), :where(), :not(), :has()
These functional pseudo-classes are common in modern CSS, and they have rules that surprise people. Let’s make them predictable.
:where() Has Zero Specificity
Everything inside :where() counts as 0 specificity.
This is amazing for writing “helpful defaults” that are easy to override.
/* :where(...) adds 0, so this is effectively (0,0,1) because of "button" */
:where(.toolbar .button) button {
border-radius: 999px;
}
/* (0,1,1) beats the :where rule easily */
.theme button.primary {
border-radius: 12px;
}
/* (0,2,1) even more specific */
.theme .toolbar button.primary {
border-radius: 4px;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
font-family: system-ui, Arial, sans-serif;
padding: 18px;
max-width: 980px;
}
.toolbar {
display: flex;
gap: 10px;
align-items: center;
padding: 12px;
border: 3px solid #111;
border-radius: 14px;
background: #fff;
}
button {
border: 2px solid #111;
border-radius: 14px;
background: #f2f2f2;
padding: 10px 14px;
font: inherit;
cursor: pointer;
}
The selector :where(.toolbar .button) button looks “big”, but :where(...) contributes
nothing.
It’s designed to keep specificity low.
:is(), :not(), and :has() Use the Most Specific Argument
For :is(), :not(), and :has(), the browser looks at what’s inside the
parentheses and uses the
most specific selector in there as the contribution.
In other words: adding a very specific option inside :is() can “upgrade” the whole selector’s
specificity.
/*
Specificity:
.card -> (0,1,0)
:is(.title, #promo) -> max of (.title = 0,1,0) and (#promo = 1,0,0) => (1,0,0)
Total => (1,1,0)
*/
.card :is(.title, #promo) {
outline: 4px solid #111;
}
/*
Specificity:
.card -> (0,1,0)
:is(.title, .featured) -> max is (0,1,0)
Total => (0,2,0)
*/
.card :is(.title, .featured) {
outline: 4px dashed #111;
}
/*
:not(...) also uses the most specific argument.
Here :not(#promo) contributes (1,0,0), so:
.card -> (0,1,0)
.title -> (0,1,0)
:not(#promo) -> (1,0,0)
Total => (1,2,0)
*/
.card .title:not(#promo) {
outline: 4px double #111;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
font-family: system-ui, Arial, sans-serif;
padding: 18px;
max-width: 980px;
}
.card {
border: 3px solid #111;
border-radius: 14px;
padding: 14px;
background: #fff;
display: grid;
gap: 10px;
}
.row {
display: grid;
gap: 10px;
}
.pill {
border: 2px solid #111;
border-radius: 999px;
padding: 8px 12px;
background: #f2f2f2;
}
.title (class)#promo.title (ID + class).featured (class)
Big takeaway: :is(), :not(), and :has() don’t have a fixed
specificity.
They “inherit” specificity from the most specific thing inside them.
A Note on :has()
:has() is especially powerful because it matches an element based on what’s inside it.
Specificity-wise, it follows the same “most specific argument” rule as :is() and :not().
/*
Specificity:
.card -> (0,1,0)
:has(.badge) -> (0,1,0)
Total => (0,2,0)
*/
.card:has(.badge) {
box-shadow: 0 0 0 6px #111;
}
/*
If we used :has(#vip) instead:
:has(#vip) contributes (1,0,0) and becomes a lot stronger.
Total would be (1,1,0)
*/
.card:has(#vip) {
box-shadow: 0 0 0 6px #111;
}
/*
A lower-specificity alternative is to keep selectors simple,
and add a class in the HTML when possible.
*/
.card.is-featured {
box-shadow: 0 0 0 6px #111;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
font-family: system-ui, Arial, sans-serif;
padding: 18px;
max-width: 980px;
}
.grid {
display: grid;
gap: 14px;
}
.card {
border: 3px solid #111;
border-radius: 14px;
padding: 14px;
background: #fff;
display: grid;
gap: 10px;
}
.badge {
width: fit-content;
border: 2px solid #111;
border-radius: 999px;
padding: 4px 10px;
font-size: 13px;
background: #f2f2f2;
}
.badgeCard with a badge inside.
#vipCard with an ID badge inside.
Card featured via class (no :has needed).
Learn more about :is() and :where() in the CSS :is() and :where() Pseudo-Classes Interactive Tutorial.
Learn more about :not() in the CSS :not Interactive Tutorial.
Learn more about :has() in the CSS :has Pseudo-Class Interactive Tutorial.
Inline Styles and !important
Two things can cut the specificity line and yell “I go first!”:
-
Inline styles using the
styleattribute. These beat normal author stylesheet rules. -
!importantmakes a declaration outrank non-important declarations. If both declarations are!important, then specificity (and source order) matter again.
Beginners often reach for !important as a quick fix. It works… but it can create a “specificity arms
race”.
Use it intentionally and sparingly.
/* This won't win against inline style without !important */
#demo {
color: seagreen;
}
/* !important can beat inline styles for the same property */
#demo {
color: seagreen !important;
}
/* If both are !important, specificity matters again */
.message#demo {
color: rebeccapurple !important;
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
font-family: system-ui, Arial, sans-serif;
padding: 18px;
max-width: 980px;
}
.box {
border: 3px solid #111;
border-radius: 14px;
padding: 14px;
background: #fff;
display: grid;
gap: 10px;
}
.message {
border: 2px solid #111;
border-radius: 12px;
padding: 12px;
background: #f2f2f2;
}
In the playground above, the inline style sets color: tomato;.
The first snippet can’t beat it. The second snippet can (because !important).
The third snippet shows that if multiple !important rules exist, you’re back to specificity again.
Learn more about !important in the CSS !important Interactive Tutorial.
How to Avoid Specificity Problems
Specificity isn’t evil. But high specificity tends to make CSS harder to maintain because it becomes harder to override later. Here are practical strategies to keep your CSS sane.
Prefer Classes Over IDs for Styling
Use IDs for JavaScript hooks, anchors, or unique labeling if you want, but styling with IDs often forces you to fight with more IDs later.
Keep Selectors Short and Reusable
A selector like .card .title is usually plenty.
A selector like body .page .content .card .header .title is a future debugging story.
Use :where() to Create Low-Specificity Defaults
:where() is a great tool for “default styling that is easy to override”.
It’s like a specificity “volume knob” set to zero.
Use Layers to Control Order Instead of Increasing Specificity
If you’re comfortable with modern CSS, @layer can help you control cascade order without cranking
selector specificity.
Even as a beginner, it’s good to know this option exists.
@layer base, components, utilities;
@layer base {
.button {
border-radius: 14px;
background: #f2f2f2;
}
}
@layer components {
.button {
background: #d6f5ff;
}
}
@layer utilities {
.button {
background: #ffe1ee;
}
}
@layer base, components, utilities;
@layer utilities {
.button {
background: #ffe1ee;
}
}
@layer components {
.button {
background: #d6f5ff;
}
}
@layer base {
.button {
background: #f2f2f2;
}
}
*,
::before,
::after {
box-sizing: border-box;
}
.wrap {
font-family: system-ui, Arial, sans-serif;
padding: 18px;
max-width: 980px;
}
.panel {
border: 3px solid #111;
border-radius: 14px;
padding: 14px;
background: #fff;
display: grid;
gap: 10px;
}
.button {
border: 2px solid #111;
padding: 10px 14px;
font: inherit;
cursor: pointer;
}
With layers, you can often win the cascade by ordering layers, not by writing large selectors.
Layers are about cascade order, not specificity points. Notice how we can change which layer wins without changing selector complexity.
Practice: Calculate Specificity for Any Selector
Let’s practice with selectors you’ll actually see. The main goal is to build the habit: count IDs, then classes/attributes/pseudo-classes, then elements/pseudo-elements.
Practice Examples With Explanations
Example 1: .nav a
.navis a class → B + 1ais an element → C + 1
Total specificity: (0,1,1)
Example 2: ul.menu li a:hover
ul,li,aare elements → C + 3.menuis a class → B + 1:hoveris a pseudo-class → B + 1
Total specificity: (0,2,3)
Example 3: #app .card[data-state="open"] h2::before
#appis an ID → A + 1.cardis a class → B + 1[data-state="open"]is an attribute selector → B + 1h2is an element → C + 1::beforeis a pseudo-element → C + 1
Total specificity: (1,2,2)
Specificity Order Summary
- Specificity is a comparison of (IDs, classes/attributes/pseudo-classes, elements/pseudo-elements).
- The selector with the higher A wins. If A ties, compare B. If B ties, compare C.
- If specificity ties, later source order wins.
-
:where()contributes zero specificity. -
:is(),:not(), and:has()contribute the specificity of their most specific argument. -
Inline styles and
!importantcan override the normal specificity flow, so use them intentionally.
CSS Specificity Conclusion
Specificity is a fundamental part of how CSS works, and understanding it will help you write more effective styles. It’s not about “winning” or “losing” – it’s about knowing how the browser decides which styles to apply when multiple rules target the same element.
