You typed !important again. The third-party reset won, your override didn't stick, and !important made the red squiggle go away. But that fix treats a structural problem as a selector problem — it never actually holds.

CSS @layer has been shipping since Chrome 99, Firefox 97, and Safari 15.4 (early 2022). It adds explicit, author-controlled ordering of concerns to the cascade. No polyfill, no build step. The migration is incremental, and the safety net (unlayered code wins) is built in.

Why You Keep Losing the Specificity War

Picture your stylesheet's contents thrown into one room: the third-party reset, your base typography, component styles, utility classes, that library you imported. They're all in the same cascade, negotiating through one currency — selector weight. Whoever writes the heavier selector wins.

There's no ordering of concerns, just a brawl refereed by specificity. When an imported section > main .btn outweighs your .btn, your only moves are to out-specify it or detonate !important. You're not fixing anything. You're escalating an arms race you started by having concerns share a room with no rules.

One Line That Changes the Currency

@layer lets you name layers and fix their priority order upfront. A rule in a higher layer beats a rule in a lower layer regardless of specificity.

/* later layers win — declare the order once, at the top */
@layer reset, base, components, utilities;

A bare .btn in utilities now beats section > main > article .btn in base — not because you found a heavier selector, but because the layer order says utilities outrank base, and the cascade checks layer order before it ever counts specificity.

Specificity still breaks ties within a layer. Between layers, your declared order is law. The currency changed.

The Third-Party Reset Problem, Gone

Every project that pulls in a reset, Tailwind's preflight, or a component library has fought this fight: some imported rule outweighs yours, and you write overrides to undo work you didn't author.

/* shove third-party styles into low layers */
@import url("reset.css") layer(reset);
@import url("ui-library.css") layer(thirdparty);

/* yours, higher — wins automatically */
@layer app {
  a      { color: var(--color-link); }
  button { border-radius: var(--radius-sm); }
}

No !important. No escalation. Third party goes low, your code goes high, low always loses. The rule is written once, at the top, where the next person can read the whole hierarchy in five seconds.

The Four-Layer Pattern That Fits Most Apps

@layer reset, base, components, utilities; /* lowest → highest */

@layer reset { *, *::before, *::after { box-sizing: border-box; } }

@layer base { body { font-family: var(--font-sans); line-height: 1.6; } a { color: var(--color-link); } }

@layer components { .card { border-radius: var(--radius-md); padding: 1.5rem; } .btn { display: inline-flex; padding: 0.5rem 1rem; } }

@layer utilities { .hidden { display: none; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; } }


When `.card` and `.hidden` collide on one element, utilities wins — not because of a cleverer selector, but because the architecture says so.

## The Unlayered Escape Hatch

Styles written outside any named layer sit in an implicit unlayered group that outranks every named layer. This feels backwards the first time, but it's deliberate — it makes adoption safe.

```css
@import url("reset.css") layer(reset);

/* unlayered — still beats the reset, exactly like before */
a { color: var(--color-link); }

Your existing, un-migrated CSS keeps behaving precisely as it does today. You adopt incrementally: wrap the third-party imports first, confirm the reset stops fighting you, then move your own styles into layers whenever you're ready.

The New Question in DevTools

One honest cost: debugging gains a dimension. "Why is this rule losing?" used to mean "find the higher-specificity rule." Now it can also mean "which layer is it in?" Chrome DevTools already groups the Styles panel by layer, so it's visible — but it's a new place to look.

The upside outweighs it. Once layers are declared, you stop tracing specificity chains and start asking "is this rule in the right layer?" — a question with a clear, checkable answer.

So Put the !important Down

That sandbag you reached for treats a structural problem as a selector problem, which is why it never actually holds. Specificity battles are the symptom. The disease is a stylesheet where every rule negotiates with every other rule through selector weight alone — and @layer adds the dimension the cascade was always missing: explicit, author-controlled ordering of concerns.

It's in Chrome 99, Firefox 97, and Safari 15.4 — shipping since early 2022. No polyfill, no build step. The migration is incremental and the safety net (unlayered wins) is built in.

The takeaway for tomorrow: the next time your finger hovers over !important, ask whether a layer boundary is the real fix. It usually is.

What's the worst specificity hack still load-bearing in your codebase right now — the #app #main .thing.thing chain, the !important nobody dares touch? Confess it in the comments and let's figure out which layer it belongs in.