Two selectors target the same element with conflicting rules but equal specificity. Which one wins and why?

Intermediate10 min interview
Skills tested:
Reciting the cascade resolution order in fullApplying source order as the final tiebreakerKnowing that HTML class attribute order is irrelevantRecognising that bundler import order defines source orderIdentifying which earlier cascade steps could pre-empt the tie

Advertisement

🧩 Scenario

This is the everyday version of the specificity question. In a component library, .btn and .btn-primary are both single classes with identical specificity, and which colour the button ends up with depends entirely on which rule the bundler emitted last. Change an import order in an entry file, or let a code splitter hoist a chunk differently, and a button silently changes colour with no CSS diff to explain it. Understanding that source order is the tiebreaker is what makes that class of bug findable.

Architecture Walkthrough

Source Order Is the Final Tiebreaker

When two declarations conflict and everything above specificity has already tied, the browser applies the one that appears last in source order. Not the first, and not the more "specific-looking" one. Last wins.

So given .btn { color: blue } followed by .primary { color: red }, an element matching both renders red, because .primary comes later in the stylesheet. Both selectors are (0, 1, 0), so specificity cannot break the tie and the cascade falls through to document order.

The Order of Classes in the HTML Is Irrelevant

This is the misconception worth stamping out first. class="primary btn" and class="btn primary" produce identical results. The class attribute is an unordered set of tokens; it expresses which rules match, not which rule wins.

The consequence is significant for utility-first CSS. In Tailwind, writing class="text-red-500 text-blue-500" does not give you blue because blue is listed second. You get whichever of the two rules Tailwind's generated stylesheet emits later, which is determined by its internal ordering, not by your markup. This is exactly why utility frameworks ship conflict-resolution helpers such as tailwind-merge: the cascade cannot resolve the intent, so the class list has to be deduplicated before it reaches the DOM.

Source Order Means Bundle Order

In a real project, "source order" almost never means the order you see in the file you are editing. It means the order of the declarations in the final concatenated CSS the browser parses.

That order is set by your build pipeline: the sequence of @import statements in the entry stylesheet, the order in which JavaScript modules import their CSS, how a bundler hoists or splits chunks, and where a CSS-in-JS runtime injects its <style> tags. Two rules in two different files have a well-defined relative order only after the bundler decides it. This is the mechanism behind one of the more frustrating bug reports in frontend work: a component's appearance changes after a refactor that touched no CSS at all, because an import moved and one rule now lands after another.

It is also why @layer is such a meaningful addition. Layers let you declare precedence explicitly and once, at the top of your entry file, so that the winner no longer depends on bundler behaviour you do not fully control.

What Could Have Pre-empted the Tie

Before concluding "they tie, so last wins," verify that the earlier cascade steps genuinely tied. Origin and importance are resolved first, so an !important on either rule ends the comparison immediately. Cascade layers come next, so if the two rules live in different @layers, layer order decides regardless of source position. Inline styles are resolved after layers and before specificity, so a style attribute defeats both rules.

There is also a subtler case that looks like a source-order surprise but is not: shorthand versus longhand. A later background: linear-gradient(...) resets background-color to its initial value as part of expanding the shorthand, even though the two declarations name different properties. The same happens with font resetting font-weight, or border resetting border-color. The conflict is real but it is happening at the longhand level after shorthand expansion, not through any specificity or ordering exception.


Key Code Explained

/* Equal specificity (0,1,0) -> the LAST rule wins */
.btn {
  color: blue;
}
.primary {
  color: red; /* applies */
}
/* <button class="primary btn"> and <button class="btn primary">
   both render RED. The class attribute order is irrelevant. */

/* Identical selector, later declaration wins */
.badge {
  color: navy;
}
.badge {
  color: crimson; /* applies */
}

/* An earlier cascade step pre-empts the tie entirely */
@layer base, components;

@layer components {
  .card {
    padding: 8px; /* applies: later layer wins regardless of source order */
  }
}
@layer base {
  .card {
    padding: 24px;
  }
}

/* !important is resolved before specificity or source order */
.first {
  color: green !important; /* applies even though .second comes later */
}
.second {
  color: purple;
}

/* Shorthand resets longhands, which LOOKS like a source-order oddity */
.panel {
  background-color: red;
}
.panel {
  background: linear-gradient(#fff, #eee);
  /* background-color is reset to transparent by the shorthand expansion */
}

/* Reverse it and the red survives, because background-color is set last */
.panel-fixed {
  background: linear-gradient(#fff, #eee);
  background-color: red; /* wins for that one longhand */
}

The @layer block is the most instructive part. The base layer rule appears later in the file yet loses, because layer order is resolved before source order is ever considered. It is the clearest demonstration that "last wins" is the final fallback, not a general rule.

The .panel pair is the other one to remember. Nothing about specificity or ordering is unusual there, but the symptom, a colour vanishing when an unrelated-looking property is added, sends people hunting for cascade exceptions that do not exist. The real mechanism is shorthand expansion writing to every longhand it owns, including the ones you did not mention.


Tradeoffs

Cascade stepResolved whenBeats source order?
Origin + !importantFirstYes
Cascade layers (@layer)SecondYes
Inline style attributeThirdYes
Specificity tupleFourthYes
Source orderLastIt is the last resort
HTML class attribute orderNever consultedNo effect at all

What Interviewers Actually Check

  • Whether you say the last rule wins rather than the first
  • Whether you know the HTML class attribute order has no bearing on the outcome
  • Whether you connect source order to bundle order rather than to the file you are editing
  • Whether you check that the earlier cascade steps genuinely tied before invoking source order
  • Whether you can recognise shorthand-resets-longhand as a distinct mechanism rather than a cascade exception

Follow-Up Questions

  1. How does tailwind-merge resolve conflicting utility classes, and why can the cascade not do that job itself?
  2. If a CSS-in-JS library injects <style> tags at runtime, where in source order do those declarations land relative to your static stylesheet?
  3. How would you use @layer to make a third-party stylesheet reliably overridable without touching specificity?
  4. Two identical declarations differ only in that one is inside a @media query that currently matches. Which applies, and does the media query affect specificity?
  5. How do CSS Modules or scoped styles change the reasoning here, given that they rewrite class names at build time?

Common Candidate Mistakes

  • Claiming the class listed later in the HTML class attribute wins, when the attribute is an unordered token set and never influences the cascade
  • Saying the first matching rule wins, which inverts the actual behaviour on an exact tie
  • Reasoning about source order from the file on screen instead of the bundled output, and therefore missing bugs caused by import order, chunk splitting, or runtime style injection
  • Missing that a later shorthand resets longhands it owns, so adding background silently clears a previously set background-color
  • Jumping straight to "they tie, so last wins" without first confirming that importance, layer order, inline styles, and specificity all actually tied

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you recite the cascade steps in order from origin and importance through to source order?
  • Can you explain why the last declaration wins on an exact tie?
  • Can you explain why the HTML class attribute order has no effect?
  • Can you describe how bundler import order determines the practical winner?
  • Can you explain how a later shorthand can silently reset an earlier longhand?

Summary

When two declarations conflict at equal specificity, equal importance, the same origin, and the same cascade layer, the one that appears last in source order applies. The order of class names in the HTML class attribute is never consulted, which is why utility-first frameworks need dedicated merge helpers instead of relying on markup order to express intent.

In practice, source order means the order of declarations in the bundled stylesheet the browser parses, not the order in the file you are editing. Import sequence, chunk splitting, and runtime style injection all determine it, which is how a component's appearance can change after a refactor that touched no CSS. @layer is the antidote: it declares precedence explicitly and once, so the winner stops depending on build behaviour.

Before concluding that source order decided a conflict, confirm the earlier steps tied. Origin and !important are resolved first, then cascade layers, then inline styles, then specificity. And watch for shorthand expansion, where a later background or font declaration resets longhands you set earlier. That looks like a cascade exception but is simply the shorthand writing to every longhand it owns.

Frequently Asked Questions

Does the order of classes in the HTML class attribute matter?

No. class="b a" and class="a b" resolve identically. Only the order of the rules in the stylesheet matters, which is why utility frameworks cannot resolve conflicts by attribute order.

What if the two rules are in different files?

What matters is their final order in the concatenated output the browser sees, which is determined by import order in your bundler, not by filename or directory.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement