@layer Browser Support and Polyfills

Every team that adopts cascade layers eventually asks the same nervous question: what happens to the 3% of visitors on an old iPad still running Safari 15.2? This page, part of Browser Support, Compatibility & Migration, answers it precisely — how CSS cascade layers resolve where they are supported, exactly what an older engine does when it meets a @layer block it does not understand, and the three shipping strategies (a PostCSS polyfill, an @supports feature gate, or a no-polyfill Baseline-only build) that let you adopt layers without stranding anyone.


Two rendering paths for a @layer block A branching diagram. Author CSS containing a @layer block splits into two paths. A browser that supports @layer registers the layer order and applies correct precedence. A browser without support treats @layer as an unknown at-rule, discards the whole block, and applies only the unlayered fallback rules. Author CSS contains a @layer block Browser supports @layer Chrome 99+, Safari 15.4+, Firefox 97+ Browser lacks @layer Safari < 15.4, Chromium < 99 Registers layer order later layer beats earlier layer, specificity only breaks ties within one layer Unknown at-rule discarded whole @layer block is skipped; only unlayered fallback rules reach the cascade

Concept and spec reference

@layer is defined by the CSS Cascading and Inheritance specification, which introduces layers as an ordering axis that the browser consults before specificity. In a supporting engine, the canonical stack used throughout this site —

/* One upfront declaration fixes precedence for the whole document.
   In a supporting browser, 'overrides' beats 'utilities' beats 'components'…
   regardless of selector weight. */
@layer reset, base, themes, components, utilities, overrides;

— is a hard contract. The reason browser support matters at all is that this contract is only honoured by engines that implement the feature. Everywhere else, the @layer at-rule is simply not recognised.

Baseline status

@layer is Baseline Widely Available. The three engines shipped support within weeks of each other in early 2022: Firefox 97 (February 2022), Chrome and Edge 99 (March 2022), and Safari 15.4 (March 2022). An interoperable feature crosses into “Widely Available” 30 months after the last major engine ships it, which for @layer landed in late 2024. In practical terms, as of this writing every current and recent release of every major browser resolves layers natively; the only exposure is residual traffic on frozen or unpatched old devices.

How the browser resolves @layer — and what unsupported engines do

The split hinges on one line in the CSS parsing model: an unrecognised at-rule is a parse error, and the recovery rule is to discard it along with its block.

In a supporting browser, the parser recognises @layer, registers each name in first-seen order, and files the rules inside into their named slots. When two declarations target the same element, layer declaration order is checked first; specificity is only consulted between two rules in the same layer.

In a non-supporting browser, the parser reaches @layer, does not recognise the identifier, and applies error recovery: it consumes and throws away everything up to and including the matching closing brace. The consequence is stark and worth stating plainly — the rules inside a @layer block do not lose the cascade in an old browser, they never enter it. Consider:

/* Old Safari sees this @layer block, fails to parse @layer,
   and deletes the whole thing — .btn never gets a background here. */
@layer components {
  .btn { background: rebeccapurple; }
}

/* This unlayered rule is parsed normally by BOTH old and new browsers.
   In old Safari it is the ONLY .btn rule that survives. */
.btn { background: navy; }

A modern engine applies rebeccapurple (the layered rule, resolved by layer order); an old engine applies navy (the only rule it managed to parse). This asymmetry is the entire basis of the progressive-enhancement strategy — and the reason a bare, unlayered fallback is non-negotiable for any team still serving pre-2022 Safari.

Practical usage patterns

Pattern 1 — PostCSS polyfill

When a meaningful share of your audience predates Baseline and you need the exact layered precedence to hold, transform the CSS at build time. The build-pipeline layer automation workflow slots @csstools/postcss-cascade-layers into your PostCSS chain; it flattens layers and rewrites selectors so layer order is re-encoded as specificity.

// postcss.config.mjs
export default {
  plugins: [
    // Reads your @layer order, then rewrites selectors so later layers
    // carry higher specificity — reproducing precedence for engines
    // that cannot parse @layer natively.
    ['@csstools/postcss-cascade-layers', {
      // 'warn' surfaces the specificity changes it makes so you can review them.
      onImportLayerRule: 'warn',
    }],
  ],
};

The cost is real: every selector’s specificity rises, and the source @layer structure is gone from the shipped file. Full setup, Browserslist targeting, and removal criteria live in configuring the PostCSS cascade layers polyfill.

Pattern 2 — @supports feature gate

When you want native layers for modern engines but only a graceful plain fallback for the stragglers — no build transform, no specificity inflation — gate the enhancement in a CSS feature query.

/* Fallback first: an unlayered rule every browser can parse. */
.card { border: 1px solid #ccc; }

/* Enhancement: applied only by engines that understand the @layer at-rule.
   Old Safari skips the whole @supports block. */
@supports at-rule(@layer) {
  @layer components {
    .card { border: 1px solid var(--color-border); box-shadow: var(--shadow-sm); }
  }
}

The at-rule() form of @supports is itself newer than @layer, so in the oldest engines the entire block is ignored and the fallback stands — exactly the desired outcome. This pattern is developed in full in progressive enhancement for older Safari.

Pattern 3 — no-polyfill, Baseline-only

The strategy most teams should choose in 2026: ship native @layer with nothing wrapped around it. If your analytics show negligible traffic below the Baseline threshold, a polyfill is pure overhead — extra build time, higher specificity, a larger stylesheet — protecting a fraction of a percent of sessions.

/* No gate, no transform. This is the whole story for a modern audience. */
@layer reset, base, themes, components, utilities, overrides;

@layer components {
  :where(.btn) { padding: 0.5rem 1rem; background: var(--color-primary); }
}

The discipline here is not code, it is measurement: you commit to a support matrix, verify traffic against it, and revisit on a schedule rather than carrying a polyfill “just in case” indefinitely.

Interaction with adjacent features

Browser support does not stand alone; it entangles with the rest of your @layer architecture:

Build-pipeline layer automation is where the polyfill actually runs. The same PostCSS pass that concatenates and orders your layer files is the natural home for the compatibility transform, and its Browserslist config is the single source of truth for which engines you are compensating for.

The role of !important in layers interacts subtly with the polyfill: because the polyfill re-expresses layer order as specificity, the normal !important inversion between layers is one of the behaviours most likely to diverge from native rendering. Audit any !important rules before trusting a polyfilled build.

Resolving third-party CSS conflicts via @import ... layer(vendor) has its own support wrinkle: the layer() keyword on @import shipped alongside @layer, so an old engine that ignores the layer block also ignores the layer() annotation on the import — but it still loads the imported file as unlayered CSS, which can then win unexpectedly.

DevTools and Browserslist diagnostic workflow

Two tools tell you whether your support strategy is holding: Browserslist defines the target, and DevTools confirms the behaviour on both sides of the Baseline line.

First, encode your matrix so tooling and analytics agree:

# Print exactly which engines your query resolves to — run this before
# deciding whether any pre-@layer browsers are still in scope.
npx browserslist "> 0.5%, last 2 versions, not dead"

Then inspect the native path in a modern browser:

  1. Open DevTools → Elements → Styles.
  2. Select an element styled by a layered rule.
  3. Use the Cascade Layers grouping (Chrome 99+) to confirm the rule appears under the expected @layer, not under (unlayered).

Finally, confirm the fallback path. In Safari’s Technology Preview or via a device-lab build, or by using Chrome DevTools Rendering → emulate CSS feature where available, load the page and verify the unlayered fallback renders acceptably when the @layer block is dropped. If you polyfill instead, inspect the built file and confirm the :not(#\#) specificity chains are present on later-layer selectors.

Migration checklist

Follow these steps to pick and ship the right compatibility strategy rather than reaching reflexively for a polyfill:

  1. Define the support matrix as a Browserslist query. Commit it to package.json or .browserslistrc so the build and your analytics dashboards reference the same target.
  2. Pull real traffic for pre-Baseline engines. Query your analytics specifically for Safari below 15.4 and Chromium below 99. A number, not a guess, drives the decision.
  3. If pre-Baseline traffic is negligible, ship Baseline-only. No polyfill, no gate. Native @layer and nothing else.
  4. If a small slice of old Safari matters, use the @supports gate. Wrap enhancements in @supports at-rule(@layer) and provide an unlayered fallback declaration for every enhanced property.
  5. If a large legacy audience needs exact precedence, add the PostCSS polyfill. Configure @csstools/postcss-cascade-layers and let Browserslist scope it.
  6. Verify both paths in DevTools. Confirm the Cascade Layers view in a modern engine and the fallback rendering in an emulated or real old engine.
  7. Schedule removal. Record the traffic threshold at which the polyfill or gate comes out, and diarise a review so the compatibility code is not carried forever.

Edge cases and gotchas

The layer() import annotation is dropped, but the file still loads

An old browser ignores @layer, and it also ignores the layer() keyword on an @import. What it does not ignore is the import itself: @import url('vendor.css') layer(vendor) still fetches and applies vendor.css as ordinary unlayered CSS in that engine. A file you intended to demote to the lowest layer can suddenly outrank everything. Where this matters, gate the import or supply the vendor styles at their intended weight in the fallback.

The polyfill raises specificity for everyone, not just old browsers

@csstools/postcss-cascade-layers rewrites your CSS at build time; the transformed file ships to all visitors, modern engines included. The :not(#\#) specificity chains it injects are present in the bytes a Chrome 130 user downloads too. This is fine functionally but means you cannot reason about your production CSS’s specificity from the source — a gotcha when debugging a leak.

@supports (selector(...)) does not test @layer

@supports has several forms, and only @supports at-rule(@layer) tests at-rule support. Reaching for @supports (display: grid) or a property query to infer layer support is a false proxy — a browser can support grid and not @layer. Use the at-rule() form or none at all.

Nested layers degrade the same way, all at once

If an old browser cannot parse @layer, it cannot parse a nested @layer components { @layer core { … } } either. The entire outer block — every nesting level inside it — is discarded together. There is no partial degradation where the inner rules survive; the fallback must cover everything the outer block would have styled.

Browser compatibility

Browser @layer support Notes
Chrome / Edge 99+ (March 2022) Baseline Widely Available
Firefox 97+ (February 2022) Baseline Widely Available
Safari 15.4+ (March 2022) Baseline Widely Available
Safari iOS 15.4+ (March 2022) Frozen-OS devices are the main pre-Baseline concern
Samsung Internet 19+ Tracks Chromium 99

The @import ... layer() syntax and the @supports at-rule() feature query share these same version thresholds, since all three landed in the 2022 cascade-layers rollout. For any engine below the line, choose one of the three patterns above rather than assuming graceful degradation.

FAQ

Is @layer safe to use in production without a polyfill in 2026?

For most teams, yes. @layer reached Baseline Widely Available in mid-2022, meaning every current release of Chrome, Edge, Firefox, and Safari supports it, and well over two years have passed since that milestone. You only need a polyfill if your analytics show meaningful traffic from Safari below 15.4 or Chromium below 99 — otherwise a Baseline-only build with no polyfill is the correct default.

What does a browser without @layer support do with a @layer block?

An unsupported browser treats @layer as an unknown at-rule. Per the CSS error-handling rules, it discards the entire at-rule and everything inside its braces. The declarations never enter the cascade at all — they are not demoted, they are dropped. That is why an unlayered fallback declaration placed after the layer block is the key to progressive enhancement: it is the only rule the old browser sees.

Does the PostCSS polyfill produce output identical to native @layer?

Not byte-for-byte, but functionally close. @csstools/postcss-cascade-layers flattens your layers and rewrites selectors so that layer order is encoded as specificity, appending :not(#\#) chains to boost later layers. The visible cascade outcome matches native behaviour for the common cases, but the transform raises every selector’s specificity, so any unlayered author style that relied on a low specificity to lose can behave differently. Test the polyfilled build; do not assume parity.

Can I detect @layer support from CSS itself?

Yes, with @supports at-rule(@layer) { ... }. This feature-query form tests whether the browser understands the @layer at-rule and only applies the wrapped rules when it does. It is the CSS-native alternative to a JavaScript build-time polyfill and is ideal when you want to ship enhanced styles to modern engines while leaving a plain fallback for older Safari.


Up: Browser Support, Compatibility & Migration