Door 2 – Mastering Specificity | CSS Adventskalender
Skip to content

Door 2 – Mastering Specificity

Published: at 07:00 AM

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:

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:

  1. Selector A is too specific
  2. Selector B must be even more specific
  3. Selector C must be more complex than B
  4. In the end, !important wins

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:

  1. Variant Destruction: A .button.ghost (which should be transparent) is now forced to have the gray background when disabled.
  2. Context Blockade: If a button on a dark background (e.g., in the footer) needed a different disabled color, it is ignored. The global !important always wins.
  3. 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.


☕ Buy me a coffee

If you enjoy my articles and they help you in your work, I would appreciate a "coffee" and a few kind words from you.

Buy me a coffee

Previous Post
Door 3 – Using Modern Selectors
Next Post
Door 1 – Understanding the Cascade

Comments