Override CSS class: Specificity, cascade & !important rules

Contents
Overriding a CSS class isn't about writing 'stronger' CSS, it's about understanding the exact math the browser uses to pick a winner between competing rules. Most override bugs aren't specificity failures at all; they're cascade order or load order failures mistaken for specificity problems.
We once spent an afternoon tracing why a custom class kept losing to a component library's class with identical specificity: the culprit was stylesheet load order, not selector weight.
Here's how the mechanics actually work, so you can force a style to apply without reaching for !important as a first resort. Before diving into override mechanics, it helps to understand how CSS fits into frontend development alongside HTML, JavaScript, and TypeScript.
How to override a CSS class (Quick answer)
Overriding a CSS class comes down to winning a specificity tuple comparison, not writing more code than the original rule. Every selector resolves to a tuple like (0,1,0,0) for a class, and the browser picks the higher tuple.
When two rules tie, cascade order decides, and the later rule in the stylesheet wins. We've debugged production stylesheets where a component library's class silently beat custom overrides due to load order, not specificity. That distinction, tuple math versus source order, is where most override bugs actually live.
Below we cover the specificity hierarchy, cascade layers, !important, and safer alternatives like custom properties. Framework-specific scoping mechanisms, like scoped styles in Vue components, can sidestep these override headaches entirely by containing specificity within a single component.
CSS specificity: How the browser scores selectors
CSS specificity is a four-part tuple, (inline, IDs, classes, elements), that the browser calculates for every selector and uses to pick a winner when two rules target the same property. MDN's specificity documentation defines the same four-column scoring the W3C Cascading and Inheritance Level 5 spec formalizes for cascade resolution.
The specificity hierarchy ranks inline styles above IDs, IDs above classes, and classes above elements. Scored as a tuple:
→ (1,0,0,0)#header→ (0,1,0,0).nav-link→ (0,0,1,0)div→ (0,0,0,1)
Attribute selectors like [type="submit"] score at the same weight as a class, (0,0,1,0), which is why .btn[disabled] outweighs a plain .btn (MDN Web Docs - CSS Specificity). Combined selectors sum their parts column by column: .card.card-title is (0,0,2,0), beating a single class every time.
The browser compares tuples left to right. Any inline style beats any number of IDs; any single ID beats any number of classes. Only a full tie falls through to cascade order.
Does an ID always override a class?
Yes, in the specificity hierarchy an ID beats a class every time, because the comparison happens column by column in the (inline styles, IDs, classes, elements) tuple, not by adding up totals. A selector scoring (0,1,0,0) always outranks (0,0,3,0), even three classes chained together. MDN's cascade documentation confirms the browser never rolls a column over into the next.
The one exception: !important skips the tuple comparison entirely and wins regardless of ID or class, which is exactly why we treat it as a maintainability debt flag, not a fix.
When specificity ties: Cascade order and load order
When two selectors score the same specificity tuple, cascade order decides the winner: the last rule declared in source order wins, regardless of which stylesheet or <style> block it lives in.
We traced a production bug where a component library's .btn-primary (0,0,1,0) kept beating a custom .btn-primary override with identical specificity. The library's stylesheet simply loaded after ours in the build pipeline.
Per W3C's CSS Cascade and Inheritance Level 5 spec, CSS Cascade Layers (@layer) let you fix this explicitly: layers are ordered independently of source position, so you declare which layer wins before specificity is ever compared.
Using !important to force an override (and when not to)
The !important flag overrides normal cascade order by boosting a declaration into a higher origin/importance tier, ahead of normal author styles, regardless of specificity tuple. Per the W3C CSS Cascading and Inheritance Level 5 spec, importance is resolved before specificity, in one of eight defined origin tiers.
We use it only against third-party CSS we can't edit, a vendor stylesheet that already ships !important and leaves no other lever. Reaching for it in our own code just relocates the conflict: the next override needs its own !important, and the cascade stops meaning anything.
The better fix, per MDN's specificity documentation, is the :where selector. It groups selectors while always resolving to zero specificity, so overrides win by cascade position, not by force.
Inline styles vs classes: Can inline override win?
Yes: an inline style beats any class selector, because the specificity hierarchy ranks inline styles above IDs, classes, and elements before importance is even considered. In tuple terms, an inline scores (1,0,0,0), while .btn-primary { color: blue; } scores (0,0,1,0), inline wins outright, per MDN's specificity documentation.
The only way a class beats an inline style is !important on the class rule, which jumps origin tier ahead of specificity entirely.
Resetting values: Initial, inherit, and unset
The initial keyword resets a property to its specification default, ignoring both the cascade and the parent element. The inherit keyword forces a property to take its parent's computed value, useful when a component library's reset rule blocks normal inheritance.
The unset keyword picks whichever behavior applies: it acts like inherit for naturally inherited properties (color, font) and like initial for everything else, per MDN's cascade and inheritance guide. We reach for unset first; it needs less context than choosing between initial and inherit manually.
Overriding Bootstrap classes and CSS custom properties
Bootstrap utility classes carry a flat specificity of (0,0,1,0), so overriding one rarely calls for !important, the real fix is often load order, not specificity. We once traced a production bug where a custom .btn-primary override kept losing to Bootstrap's own class. The override stylesheet loaded before the Bootstrap link tag, not after, so cascade order buried it regardless of selector weight.
For theme-level changes, override Bootstrap's CSS custom properties instead of fighting individual classes. Per Bootstrap's CSS variables documentation, Bootstrap 5.3 exposes its color and spacing system as --bs-* variables on :root; redeclaring --bs-primary at a narrower scope cascades to every component that reads it. Patching a CSS reset or utility class directly breaks on the next Bootstrap upgrade.
Overriding CSS classes in React and Angular
React and Angular scope styles differently, so overriding a class means fighting the framework's isolation mechanism, not just specificity math.
CSS Modules hash class names at build time, .button becomes .button_a3f1x, so a plain .button { color: red } override in another file never matches at all.
The fix is importing the same module and composing, or targeting the generated hash via a global selector. Angular's default ViewEncapsulation.Emulated adds a scoped attribute like [_ngcontent-abc] to every element, raising the effective specificity tuple past what an external stylesheet expects.
Teams that skip CSS Modules often fall back to BEM naming convention (.card__title--active) precisely because it fakes scoping through naming discipline instead of tooling, keeping overrides predictable without extra specificity weight.
FAQ: Overriding CSS classes
Does an ID always override a class in CSS?
How do I override a CSS class in React?
Can an inline style override a class?
How do I make one CSS class override another?
Does jQuery's .css() override CSS classes?
.css() writes directly to the element's inline style attribute, so it overrides any class-based rule the same way manual inline styles do. It does not touch stylesheet rules or specificity calculations at all. This makes it a quick patch for debugging, but a maintainability debt if left in production code.
