What are CSS cascade layers (@layer), and what problem do they solve that specificity alone cannot?

Advanced15 min interview
Skills tested:
Explaining where layers sit in the cascade relative to specificityDeclaring layer order up front and knowing why that mattersKnowing that later layers win for normal declarations and earlier layers win for !importantPlacing unlayered styles correctly relative to layered onesUsing @import with layer() to contain third-party CSS

Advertisement

🧩 Scenario

Cascade layers exist to solve a problem every mature codebase hits: overriding a third-party stylesheet, or a reset, without escalating specificity. Before @layer, defeating a vendor rule like .vendor-grid .cell .label meant writing something even more specific, which then had to be defeated by something more specific still. Layers let you declare once that your component styles outrank vendor styles, after which a single class beats any vendor selector no matter how deeply chained it is.

Architecture Walkthrough

Layers Sit Above Specificity in the Cascade

@layer introduces named precedence buckets that are resolved before specificity is consulted. The cascade order is: origin and importance first, then cascade layers, then inline styles, then specificity, then source order.

That placement is the whole point. Because layer order is decided before any tuple comparison happens, a selector's specificity is irrelevant across layers. A .title rule in a later layer defeats a #main .card .title rule in an earlier layer, even though the latter is (1, 2, 0) and the former is (0, 1, 0). Within a single layer, the normal cascade resumes and specificity works exactly as it always has.

Declare the Order Up Front

Layer order is established by the order in which layer names are first encountered. Relying on that implicitly is fragile, because it makes precedence depend on which file the bundler happens to emit first, the same problem cascade layers were meant to fix.

The convention is a single order declaration at the very top of your entry stylesheet, before any layer body:

@layer reset, base, components, utilities;

After that line, layers can be populated in any order, in any file, and their precedence is fixed. You can also append to a layer any number of times; all the blocks with the same name are treated as one layer at its declared position.

The mental model is architectural: resets and normalisation go in the first layer so everything can override them, design-system base element styles next, component styles after that, and single-purpose utilities last so a .mt-0 genuinely wins.

Unlayered Styles and the !important Inversion

Two behaviours are consistently misremembered, and both are worth stating precisely.

Unlayered styles win for normal declarations. Any rule not inside a @layer behaves as though it belongs to an implicit final layer that sorts after all named layers. This is deliberately backwards-compatible: adopting @layer for part of your CSS cannot cause your existing unlayered styles to start losing.

For !important, the layer order inverts. Among important declarations, the earliest layer wins, and unlayered !important loses to any layered !important. This mirrors how !important already inverts the origin order between author and user styles. Practically it means a reset layer can enforce something with !important that no later layer can defeat, which is occasionally useful and frequently surprising.

Containing Third-Party CSS

The highest-value everyday use of layers is quarantining CSS you do not control. @import accepts a layer() function, so an entire vendor stylesheet can be dropped into a low-precedence layer at import time without editing a single one of its rules:

@layer vendor, components;
@import url('vendor-ui.css') layer(vendor);

From that point on, any rule in your components layer beats any rule in the vendor stylesheet, regardless of how deeply the vendor chained its selectors. That is the capability specificity alone could never provide: specificity only lets you climb higher, while layers let you declare who wins once and stop climbing.

Anonymous layers are also available: @layer { … } with no name creates a one-off layer at that position that nothing else can append to. Useful for genuinely isolated blocks, but named layers are almost always the better choice because they can be reordered and appended to deliberately.


Key Code Explained

/* Declare order ONCE, at the top of the entry file, before any layer body */
@layer reset, base, components, utilities;

/* Import third-party CSS directly into a low-precedence layer */
@import url('vendor-ui.css') layer(vendor);

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

@layer base {
  /* High specificity, but in an EARLY layer */
  #main .card .title {
    font-size: 1rem;
    color: gray;
  }
}

@layer components {
  /* Low specificity, but in a LATER layer -> this one wins */
  .title {
    font-size: 1.5rem;
    color: black;
  }
}

@layer utilities {
  /* Last layer: single-class utilities reliably beat components */
  .mt-0 {
    margin-top: 0;
  }
}

/* Layers can be appended to from anywhere; position stays as declared */
@layer components {
  .card {
    border-radius: 8px;
  }
}

/* Unlayered styles act as an implicit FINAL layer for normal declarations */
.title {
  color: hotpink; /* beats the @layer components rule above */
}

/* For !important the order INVERTS: earliest layer wins */
@layer reset {
  .locked {
    display: none !important; /* beats an !important in a later layer */
  }
}
@layer utilities {
  .locked {
    display: block !important; /* loses, despite being in a later layer */
  }
}

The base versus components pair is the core demonstration. #main .card .title at (1, 2, 0) loses to a bare .title at (0, 1, 0) purely because of layer position. Nothing else in CSS lets a lower-specificity selector win so cleanly, and it is what makes layers an architectural tool rather than a convenience.

The !important block is the detail interviewers use to separate people who have read the spec from people who have skimmed a blog post. The inversion is easy to state and easy to forget, and it produces genuinely confusing behaviour when a reset layer pins a value that no later layer can move.


Tradeoffs

MechanismPrecedence sourceCan a low-specificity rule win?Cost
Specificity escalationSelector shapeNoAverage specificity ratchets up permanently
!importantImportance stepYesContagious; next override also needs !important
@layerDeclared layer orderYesRequires up-front architecture; modern browsers only
:where()Zero specificityN/A, lowers instead of raisesOnly helps rules you author
Source orderBundle positionOnly on exact tiesFragile, depends on the build

What Interviewers Actually Check

  • Whether you can place layers correctly in the cascade, specifically that they are resolved before specificity
  • Whether you declare layer order explicitly rather than relying on first appearance
  • Whether you know unlayered normal styles win against layered ones
  • Whether you know the !important ordering inverts across layers
  • Whether you can describe the third-party containment use case with @import ... layer()

Follow-Up Questions

  1. How do nested layers such as @layer components.buttons sort relative to their siblings and their parent?
  2. What is the relationship between cascade layers and shadow DOM encapsulation, and do they solve the same problem?
  3. If a project already uses !important extensively, what is a safe migration path to layers?
  4. How would you structure layers for a design system consumed by applications that also ship their own CSS?
  5. Do cascade layers affect the specificity reported in devtools, and how would you debug a layer-driven override there?

Common Candidate Mistakes

  • Believing a heavily chained selector in an earlier layer can beat a single class in a later layer, when layer order is resolved before specificity is even examined
  • Never writing an explicit @layer order declaration, leaving precedence dependent on which file the bundler emits first, which reintroduces the exact fragility layers were designed to remove
  • Assuming unlayered styles lose to layered ones, when for normal declarations they behave as an implicit final layer and win
  • Expecting !important declarations to follow the same layer ordering as normal ones, rather than the inverted order where the earliest layer wins
  • Putting resets in the last layer, which makes them override everything instead of being the most overridable styles in the system

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you place cascade layers correctly relative to specificity and importance in the cascade?
  • Can you write a layer order declaration and explain why it belongs at the top of the entry file?
  • Can you explain the inverted !important behaviour across layers?
  • Can you explain where unlayered styles rank for normal declarations?
  • Can you show how to wrap a third-party stylesheet in a layer at import time?

Summary

Cascade layers create named precedence buckets that the browser resolves before specificity. Because of that placement, a low-specificity selector in a later layer beats a high-specificity selector in an earlier one, which is something no amount of selector engineering could achieve. Inside a single layer, the normal cascade continues and specificity works as usual.

Layer order comes from the order names are first seen, so the convention is one declaration such as @layer reset, base, components, utilities; at the top of the entry stylesheet. After that, layers can be populated and appended to from any file in any order without changing precedence. Two behaviours are easy to misremember: unlayered normal declarations act as an implicit final layer and win, while among !important declarations the order inverts so the earliest layer wins.

The problem layers solve that specificity cannot is containment. @import url('vendor.css') layer(vendor) puts an entire third-party stylesheet below your own without editing any of its rules, so a single class of yours reliably overrides its deepest selector. That replaces the escalation spiral, where every failed override adds another class, ID, or !important, with a precedence decision you declare once and never fight again.

Frequently Asked Questions

Do unlayered styles win or lose against layered ones?

For normal declarations, unlayered styles win: they behave as an implicit final layer. For !important declarations the order inverts, so earlier layers win and unlayered !important loses to layered !important.

Does specificity still matter inside a layer?

Yes. Layers decide between layers; within a single layer the normal cascade continues, so specificity and then source order resolve conflicts as usual.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement