Specificity is a core concept of CSS and one of the most common reasons why styles don’t apply as expected. Even experienced teams stumble over it in their daily work because specificity often emerges as a byproduct rather than a conscious decision. If you understand specificity, you control CSS. If you ignore it, you fight against CSS.
In this door, we’ll look at how specificity works, where problems arise in real projects, and how to get a handle on them in the long run.
What Specificity Actually Measures
Specificity evaluates how precisely a selector targets an element. The system behind it is simple but immensely powerful.
CSS counts:
- Column A: Number of IDs
- Column B: Number of classes, attributes, and pseudo-classes
- Column C: Number of elements and pseudo-elements
The values are compared from left to right. One ID in Column A beats infinite classes in Column B.
A few examples:
h1 {} /* Specificity: (0, 0, 1) */
.title {} /* Specificity: (0, 1, 0) */
#main {} /* Specificity: (1, 0, 0) */
button[disabled] {} /* Specificity: (0, 1, 1) */
And the classic:
<button style="background: red"></button>
An inline style is considered the most specific source in the author origin and is handled by browsers in a separate column. It is often simplified as specificity (1, 0, 0, 0).
Why Specificity Often Leads to Problems
Specificity is rarely designed consciously. It arises accidentally as components are nested deeper or additional classes are added.
A typical example from real projects:
/* Design System */
.card .title {
font-size: 1.25rem;
}
/* A later adjustment */
.title {
font-size: 1.5rem;
}
At first glance, you might think the second rule wins because it comes later.
But: .card .title has a higher specificity ((0, 2, 0) vs. (0, 1, 0)).
The bug cost a team several hours.
The Actual Problem
The problem is not just a misunderstood selector. It is the fact that the specificity of every new rule makes the entire system more unstable.
Cascading Nesting: A Creeping Risk
In many codebases, you find overly nested selectors:
.layout .header .nav .item .link {
color: var(--gray-700);
}
Specificity: (0, 5, 0)
Such a selector makes it nearly impossible to cleanly implement later overrides without also increasing your own specificity.
The result is a vicious cycle:
- Selector A is too specific
- Selector B must be even more specific
- Selector C must be more complex than B
- In the end,
!importantwins
This creates technical CSS debt.
!important: Symptom, Not Solution
Many reach for !important in their daily work because a rule “just won’t apply”.
The cause is almost always specificity.
!important solves the acute problem but exacerbates the system’s instability in the long term.
A real case:
.button {
background: var(--primary);
}
.header .button {
background: var(--brand-blue);
}
.button[disabled] {
background: var(--gray-200);
}
/* A developer sets this */
.button[disabled] {
background: var(--gray-300) !important;
}
This !important override fixed one bug – but created three new ones:
- Variant Destruction: A
.button.ghost(which should be transparent) is now forced to have the gray background when disabled. - Context Blockade: If a button on a dark background (e.g., in the footer) needed a different disabled color, it is ignored. The global
!importantalways wins. - Escalation: To ever override this style again, the next developer must also use
!important. This starts a spiral that makes the code unmaintainable.
How to Actively Reduce Specificity
An important tool is the :where() selector.
:where() Sets Specificity to Zero
Example:
:where(.card .title) {
font-weight: 500;
}
No matter how deeply nested, the specificity remains 0.
This makes CSS significantly more stable, as later overrides don’t have to win a “specificity battle”.
A real comparison:
/* Before */
.card .title {
font-weight: 500;
}
/* Today */
:where(.card .title) {
font-weight: 500;
}
With the second variant, any later rule with a specificity of (0, 0, 1) (e.g., h1) can override it. This feels unusual at first, but leads to noticeably cleaner CSS.
The Strategic Approach: Designing Selectors Consciously
In large projects, I recommend the following principles:
1. One Class Per Component
Instead of:
.page .content .box .title {}
Prefer:
.component-title {}
Note: This contradicts the approach of Tailwind CSS. Tailwind relies on many small utility classes that bypass the specificity problem by all having the same low specificity, but winning through their order in the generated CSS (“Utilities Layer”). If you write classic CSS or CSS Modules, “one class per component” is the safest bet.
2. No ID Selectors in CSS
IDs generate unnecessarily high specificity and offer little value over classes.
3. Inline Styles Only for Truly Dynamic Values
Values that come from runtime logic. Not for colors. Not for layout.
4. Avoid Selectors with More Than 2 Classes
Anything above that quickly creates technical risk.
A Practical Example: Planning Specificity Correctly
A former client had the following setup:
/* Component */
.button {
padding: 0.75rem 1rem;
background: var(--primary);
}
/* Variant */
.button.is-secondary {
background: var(--secondary);
}
/* Dark Mode Override */
.dark .button {
background: var(--primary-dark);
}
The problem: .dark .button and .button.is-secondary both have specificity (0, 2, 0). Here, source order decides – and .dark .button came later in the CSS, causing it to win unintentionally.
The solution:
:where(.button) {
padding: 0.75rem 1rem;
background: var(--primary);
}
.button.is-secondary {
background: var(--secondary);
}
.dark :where(.button) {
background: var(--primary-dark);
}
By using :where(), the base specificity was set to 0 and the entire system became more stable.
Conclusion
Specificity decides which rule wins in CSS. If you ignore it, you will inevitably run into problems, especially in growing projects. If you control it, you write CSS that is predictable, stable, and maintainable in the long term.
Specificity is not a theoretical concept, but a practical tool. Used correctly, it is one of the strongest levers for a clean CSS architecture.
Comments