Tailwind Specificity & Cascade Failures
Tailwind delegates ordering to native CSS @layer plus declaration-
order rules. Conflicts trace to FIVE distinct mechanisms :
- Layer-level priority (base < components < utilities < unlayered).
- Class-level specificity (single-class selectors -> 0,0,1,0).
- Declaration order WITHIN one class (last property wins).
!importantmodifier (overrides cascade entirely).- Plugin loading order (when two plugins emit the same class).
This skill is the diagnostic flow for all five.
Companion skills :
tailwind-syntax-apply-directive:@applydeep divetailwind-impl-plugins-custom: authoring your own pluginstailwind-impl-config-v3andtailwind-impl-config-v4: where to declare custom utilitiestailwind-errors-build-failures: when the class is NOT in CSStailwind-errors-runtime: DOM-level issues (wrong attribute, React class vs className)
Quick Reference : The Layer Order
Tailwind emits a native CSS @layer block. Browser cascade resolves
in this order (HIGHER wins) :
1. Unlayered styles (HIGHEST -> "naked" CSS)
2. @layer utilities (Tailwind utilities)
3. @layer components (Tailwind components, prose, btn)
4. @layer base (Preflight reset, typography defaults)
5. Browser defaults (LOWEST)
A utility (bg-red-500) ALWAYS beats a component (prose) which
ALWAYS beats base (h1 reset). Unlayered CSS (custom rules NOT inside
@layer) ALWAYS wins over any Tailwind output. That last rule is
the most common source of "my utility doesn't work".
Decision Tree : "My Utility Is Not Applied"
Inspect the element in DevTools. Is the utility selector visible
with a strike-through ?
├── YES (something overrides it)
│ ├── Override is in :hover, :focus, :active etc
│ │ -> Add hover:/focus: variant to your utility
│ ├── Override is from an unlayered CSS rule (e.g. ".my-card { padding: 20px; }")
│ │ -> Move that rule into @layer components or remove it
│ ├── Override is a plugin-emitted component (prose-*, form-*)
│ │ -> Your utility is correct ; the component is winning
│ │ because Tailwind v3 plugin classes register in the
│ │ components layer. Add !-modifier OR a more specific
│ │ utility variant.
│ └── Override comes from a third-party CSS file (Bootstrap, etc.)
│ -> Apply the !-important modifier OR move third-party CSS
│ into a lower layer.
│
└── NO (selector not shown at all)
├── Class not in compiled CSS -> see tailwind-errors-build-failures
├── Class is in markup but DOM attribute is `class` in JSX (not className)
│ -> Fix the JSX (use className)
└── Class targets a pseudo-element you cannot style (e.g. ::-webkit-scrollbar)
-> Use vendor-specific utility or raw CSS in @layer utilities
CRITICAL : Unlayered CSS Defeats Tailwind
The most common failure. Custom CSS authored OUTSIDE any @layer
block wins over everything Tailwind emits.
Wrong
/* app.css */
@import "tailwindcss";
.card {
padding: 1rem;
background: white;
}
In markup :
<div class="card p-8 bg-zinc-100">
p-8 and bg-zinc-100 are emitted in @layer utilities. The .card
rule is UNLAYERED. Native CSS cascade : unlayered > any layered. So
padding is 1rem (from .card) NOT 2rem (from p-8).
Right (v3)
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.card {
padding: 1rem;
background: white;
}
}
Right (v4)
@import "tailwindcss";
@layer components {
.card {
padding: 1rem;
background: white;
}
}
Now .card lives in @layer components. p-8 from @layer utilities
WINS as expected.
CRITICAL : @apply Strips !important by Default (v3)
.my-card {
@apply !p-4;
}
This INTENTIONALLY does NOT preserve the !important flag in v3. The
output is padding: 1rem; not padding: 1rem !important. Per v3
docs : "By default !important flags are stripped to avoid
specificity conflicts."
Force !important (v3)
.my-card {
@apply p-4 !important;
}
v4 behaviour
v4 PRESERVES the important modifier through @apply. If you write :
.my-card {
@apply p-4!;
}
The output IS padding: 1rem !important.
CRITICAL : Important Modifier Syntax Flipped Between v3 and v4
| Version | Syntax | Example |
|---|---|---|
| v3 | !-PREFIX |
!font-bold, !bg-red-500, md:!text-xl |
| v4 | !-SUFFIX |
font-bold!, bg-red-500!, md:text-xl! |
Both produce !important in compiled CSS. The position changed
between major versions ; mixing them is the single most common
v3-to-v4 migration trap.
Wrong in v4
<div class="!font-bold"> <!-- v3 syntax, v4 ignores -->
Right in v4
<div class="font-bold!">
Right in v3
<div class="!font-bold">
Note for variant chains :
- v3 :
md:hover:!text-blue-500 - v4 :
md:hover:text-blue-500!
CRITICAL : Plugin Order in v3 plugins Array
When TWO plugins emit the same class name, the LAST loaded plugin
wins (because its CSS is appended later in @layer components /
@layer utilities).
// tailwind.config.js
module.exports = {
plugins: [
require("@tailwindcss/typography"), // emits prose, prose-* (FIRST)
require("custom-typography-plugin"), // emits prose with overrides (LAST -> WINS)
],
};
ALWAYS list plugins in ASCENDING priority order : least-specific first, most-specific last. If a third-party plugin must override your custom plugin, load the third-party plugin LATER.
CRITICAL : Declaration Order Inside @apply
Within ONE @apply line, conflicting CSS properties resolve by
DECLARATION ORDER, not specificity (specificity is the same: 0,0,1,0).
.btn {
@apply px-4 py-2 px-8; /* px-8 wins ; px-4 silently lost */
}
Output :
.btn {
padding-left: 2rem;
padding-right: 2rem;
padding-left: 2rem; /* duplicate, harmless */
padding-right: 2rem;
}
The LAST px-* directive applied is the surviving padding-left/right.
ALWAYS audit @apply lines for the same-property-twice anti-pattern.
Note : utilities targeting DIFFERENT properties survive together :
.btn {
@apply px-4 py-2 bg-blue-600 text-white; /* all four survive */
}
CRITICAL : @layer utilities vs @utility (v4)
In v4 the PREFERRED way to add custom utilities is the @utility
directive ; @layer utilities still works but has subtle differences.
/* v4 preferred */
@utility tab-4 {
tab-size: 4;
}
vs
@layer utilities {
.tab-4 { tab-size: 4; }
}
Difference :
@utilityparticipates fully in variant pipeline (hover:tab-4,md:tab-4) and overrides components correctly.@layer utilitiesemits the rule but custom utilities authored this way do NOT always handle responsive variants reliably for complex CSS.
ALWAYS prefer @utility in v4 for new custom utilities.
Decision Tree : Forcing Override Without !important
Can the layer be adjusted ?
├── My custom CSS rule -> Move into @layer components (lower priority)
│ OR move overriding utility into @layer utilities (higher)
│
├── Third-party CSS I can edit -> Wrap in @layer base or @layer components
│
├── Third-party CSS I CANNOT edit -> Use !-modifier as last resort
│ (font-bold! in v4, !font-bold in v3)
│
└── Inline style attribute on the element -> CANNOT override with class
(inline style wins)
-> Apply utility WITH !-modifier
OR change the markup
CRITICAL : Inline style Always Wins (Almost)
<div style="padding: 10px" class="p-8">
Padding is 10px. Class is ignored. ONE exception : utility with
!-modifier beats inline style.
<div style="padding: 10px" class="p-8!"> <!-- v4 syntax, wins -->
<div style="padding: 10px" class="!p-8"> <!-- v3 syntax, wins -->
ALWAYS prefer fixing the markup. The !-modifier on every class is a code smell.
@reference for Scoped Styles (v4)
Vue <style scoped>, Svelte <style>, CSS modules, and MDX inline
styles process CSS in ISOLATION from app.css. @apply inside them
fails with "Cannot apply unknown utility class" because they have
NO theme context.
Fix : @reference imports the theme without duplicating CSS output.
<style>
@reference "../../app.css";
h1 { @apply text-2xl font-bold; }
</style>
OR (for default theme only) :
<style>
@reference "tailwindcss";
h1 { @apply text-2xl font-bold; }
</style>
ALWAYS use @reference for scoped stylesheets that need @apply.
See tailwind-syntax-apply-directive for full coverage.
What This Skill Does NOT Cover
- Class is NOT in compiled CSS -> see
tailwind-errors-build-failures. - @apply patterns for component composition -> see
tailwind-syntax-apply-directive. - Custom utility/component design via plugins -> see
tailwind-impl-plugins-custom. - prefix / important: config flags (v3) -> see
tailwind-impl-config-v3. - CSS variables / @theme tokens -> see
tailwind-impl-config-v4.
References
references/methods.md: every layer + directive that affects specificity, full !-modifier syntax for both versions, plugin-order rules.references/examples.md: real cases with DevTools-level output.references/anti-patterns.md: unlayered CSS, wrong-version important syntax, @apply !important strip, plugin-order mistakes.
Official Source Links
- v4 functions and directives : https://tailwindcss.com/docs/functions-and-directives
- v3 functions and directives : https://v3.tailwindcss.com/docs/functions-and-directives
- v4 styling guide (important) : https://tailwindcss.com/docs/styling-with-utility-classes