SuperAPI AI Directory

tailwind-errors-specificity

Frontend & websites

View repository

Updated 10-15 22:18

About this skill

Tailwind Specificity & Cascade Failures

Tailwind delegates ordering to native CSS @layer plus declaration- order rules. Conflicts trace to FIVE distinct mechanisms :

  1. Layer-level priority (base < components < utilities < unlayered).
  2. Class-level specificity (single-class selectors -> 0,0,1,0).
  3. Declaration order WITHIN one class (last property wins).
  4. !important modifier (overrides cascade entirely).
  5. 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 : @apply deep dive
  • tailwind-impl-plugins-custom : authoring your own plugins
  • tailwind-impl-config-v3 and tailwind-impl-config-v4 : where to declare custom utilities
  • tailwind-errors-build-failures : when the class is NOT in CSS
  • tailwind-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 :

  • @utility participates fully in variant pipeline (hover:tab-4, md:tab-4) and overrides components correctly.
  • @layer utilities emits 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