TLDR: if your custom Tailwind class keeps getting overridden and you can't figure out why, check whether it lives inside @layer components. If it does — and something unlayered is winning — pull it out.

The Setup

I was building a glass-effect card for the content calendar board in a Key Opinion Leader management CRM (a Next.js/Tailwind app for a client).

The class was called glass-tile — dark frosted-glass background, backdrop-blur, the whole look.

I dropped it into @layer components { … } in the main stylesheet, which is exactly what you're supposed to do when you want purge-immunity. (Tailwind never purges authored @layer rules — it's the safe bucket for any class JS will toggle at runtime.)

Looked right in my head. Did not look right in the browser.

The Wall

The glass background kept losing.

Utility classes on the same element — bg-white/5, backdrop-blur-md — were winning the cascade. I'd see my glass-tile definition, clearly there, clearly correct, getting stomped by a bg-* class I'd tossed on the element three commits ago.

So naturally I did the wrong thing first.

I tried fix(css): force dark glass-tile background with explicit properties — adding property after explicit property to the rule, trying to "out-specificity" the utilities.

Did not work. Specificity wasn't the problem.

What's Actually Happening

Here's the thing about CSS cascade layers that bit me: unlayered styles beat layered styles, full stop.

Doesn't matter how specific your rule is. Doesn't matter if it comes later in the file.

If a style exists outside any @layer declaration, it wins over everything inside one.

So @layer components { .glass-tile { … } } was always going to lose to any unlayered rule targeting the same element. The cascade layer boundary is a ceiling, not a floor.

(I'd actually documented this exact trap in my Campfire notes from a different project: "unlayered beats layered → makes it worse." I just didn't connect the dots fast enough.)

The Fix That Worked

/* Before — losing the cascade */
@layer components {
  .glass-tile { … }
}

/* After — unlayered, wins */
.glass-tile { … }

That's it. One move, outside the layer. fix(css): move glass-tile outside @layer components for correct cascade.

Browser agreed immediately.

Why It Matters to Me

The frustrating part is that the REASON I put it inside @layer components was sound — purge-immunity is real, and the two-bucket pattern is genuinely how I protect runtime-toggled classes from disappearing in production builds.

But that same protection demoted it in the cascade.

That's the tension worth remembering: @layer components keeps your class alive through the build — but it also puts it below anything unlayered. If your class needs to win at runtime against other unlayered rules, living inside a layer works against you.

Now I default to: layer it for safety, move it out if something unlayered keeps winning. Document which it is, and why.