Checking Layer Support With CSS.supports in JavaScript
The natural first attempt — CSS.supports('@layer') — returns false in Chrome 130, Firefox 140 and Safari 18 alike, which sends people to the wrong conclusion. The API is not broken; it answers a different question. This page covers what each detection mechanism actually tests and when any of them is worth shipping, extending testing cascade layers across browsers within the browser support, compatibility and migration reference.
Prerequisites
You need to know that @layer reached Baseline in March 2022 — Chrome 99, Firefox 97, Safari 15.4 — and that an unsupported at-rule causes the browser to discard the rule and its entire block.
Step 1: Understand why CSS.supports cannot answer this
// All of these return false in fully supporting browsers.
CSS.supports('@layer'); // false — not a declaration
CSS.supports('@layer', 'base'); // false — not a property/value pair
CSS.supports('at-rule(@layer)'); // false — not the JS form of the syntax
// WHY: CSS.supports takes either a "property: value" declaration or, in its
// one-argument form, a condition made of such declarations and selector()
// tests. At-rules are outside its grammar entirely, so the answer is always
// false — which is indistinguishable from "not supported" and therefore a
// trap.
CSS.supports('color', 'revert-layer'); // true in supporting browsers ✓What this does: rules out the obvious approach and provides a usable substitute. The last line is the practical trick — revert-layer is a value, so CSS.supports can test it, and it shipped in exactly the same releases as @layer. Testing it is a reliable proxy.
Step 2: Probe the CSSOM for a definitive answer
// WHY a constructed stylesheet: it never touches the document, so the probe
// cannot affect rendering. If the engine does not understand @layer it
// discards the rule at parse time, and cssRules comes back empty.
export function supportsCascadeLayers() {
try {
const sheet = new CSSStyleSheet();
sheet.replaceSync('@layer probe { }');
return sheet.cssRules.length > 0 &&
sheet.cssRules[0].constructor.name === 'CSSLayerBlockRule';
} catch {
// Constructable stylesheets are themselves a 2021+ feature; a browser
// without them certainly predates @layer, so false is the right answer.
return false;
}
}What this does: tests the actual at-rule rather than a correlate. Use it when you need certainty — for example when deciding whether to fetch a polyfilled bundle, where a wrong answer costs a download.
Step 3: Prefer the CSS-only gate where the fallback is styling
/* WHY at-rule() rather than JavaScript: no script, no layout shift, no
hydration timing. The browser evaluates this while parsing. */
@supports at-rule(@layer) {
@layer components {
.card { border: 1px solid var(--color-border); }
}
}
/* WHY the negated branch is a separate, self-contained block: an engine that
does not understand @layer also discards the @layer block above ENTIRELY,
so the fallback must restate the rules rather than adjust them. */
@supports not at-rule(@layer) {
.card { border: 1px solid #d4c89a; }
}What this does: keeps detection in the language that owns the decision. Note that at-rule() inside @supports is itself newer than @layer, so an engine that supports neither takes the fallback branch — which is the correct outcome by accident, and worth confirming in your oldest target browser.
Step 4: Decide what the fallback actually does
// WHY a decision, not a reflex: each option has a real cost, and for most
// audiences the right answer is the last one.
if (!supportsCascadeLayers()) {
// Option A — fetch the polyfilled bundle. Costs a round trip for old
// browsers only, and the polyfill inflates specificity for everyone in it.
import('/assets/index.polyfilled.css');
// Option B — degrade. The layered stylesheet was discarded, so the page
// needs a complete unlayered fallback, not a patch.
// Option C — do nothing. Correct when the affected traffic is negligible,
// and by far the cheapest to maintain.
}What this does: forces the choice to be explicit. Option B is the one most often underestimated: because an unsupported @layer block is discarded wholesale, the fallback is an entire parallel stylesheet, not a few overrides.
Verification
- Run the probe in a current browser — it must return
true, andCSS.supports('@layer')must returnfalse. Seeing both confirms you are testing what you think you are. - Verify the CSS gate takes the right branch by temporarily inverting it to
@supports not at-rule(@layer)and confirming the fallback renders. - Check the fallback stylesheet is genuinely complete:
# Every selector in the layered bundle should have a counterpart in the
# fallback — an unsupported @layer block is discarded whole, not partially.
grep -o "^\s*\.[a-z0-9_-]*" dist/assets/index.css | sort -u > /tmp/layered.txt
grep -o "^\s*\.[a-z0-9_-]*" dist/assets/fallback.css | sort -u > /tmp/fallback.txt
comm -23 /tmp/layered.txt /tmp/fallback.txt- Confirm the detection is still needed. If your analytics show no pre-2022 engines, delete it.
Troubleshooting
The probe returns false in a modern browser.
: constructor.name is unreliable under aggressive minification that renames globals. Compare with CSSLayerBlockRule directly via instanceof, guarded by a typeof CSSLayerBlockRule !== 'undefined' check.
@supports at-rule(@layer) never matches.
: The at-rule() function is newer than @layer itself, so an engine may support layers but not the test. Where that matters, use the revert-layer value proxy in CSS: @supports (color: revert-layer) { … }.
The fallback stylesheet is missing most styles.
: An unsupported @layer block is discarded in full, including every rule inside it. The fallback has to be a complete parallel stylesheet, which is why generating it with the PostCSS polyfill is usually cheaper than maintaining it by hand.
Detection runs before the stylesheet loads.
: The CSSOM probe does not depend on your stylesheets — it constructs its own — so ordering does not matter for it. If you are instead inspecting document.styleSheets, that is load-order dependent and will report nothing when run too early.
A bot or preview renderer takes the fallback branch. : Older headless renderers and some link-preview services predate 2022. Check whether that traffic matters before treating it as a defect; for most sites it does not.
Complete working example
// src/lib/layer-support.js — complete, dependency-free detection.
/**
* Definitive test for @layer support.
* WHY a constructed sheet: it is never adopted into the document, so the
* probe has no rendering side effects and no cleanup.
*/
export function supportsCascadeLayers() {
if (typeof CSSStyleSheet === 'undefined') return false;
try {
const sheet = new CSSStyleSheet();
sheet.replaceSync('@layer probe { }');
if (sheet.cssRules.length === 0) return false;
// WHY instanceof with a guard: constructor.name breaks under minifiers
// that rename globals; the guard covers engines lacking the interface.
return typeof CSSLayerBlockRule !== 'undefined' &&
sheet.cssRules[0] instanceof CSSLayerBlockRule;
} catch {
return false;
}
}
/**
* Cheap proxy for the same question, usable where a constructed sheet is
* unavailable. WHY it works: `revert-layer` shipped in the same releases as
* @layer, and unlike an at-rule it IS testable by CSS.supports.
*/
export function supportsCascadeLayersProxy() {
return typeof CSS !== 'undefined' &&
CSS.supports?.('color', 'revert-layer') === true;
}
/**
* The only decision most applications need to make.
* WHY it is async and lazy: the fallback bundle must not cost anything for
* the overwhelming majority of visitors who do not need it.
*/
export async function ensureStyles() {
if (supportsCascadeLayers()) return 'native';
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '/assets/index.fallback.css';
document.head.append(link);
return 'fallback';
}/* src/styles/entry.css — the CSS-only equivalent, preferred where the
difference is purely visual. */
@supports at-rule(@layer) {
@layer reset, base, theme, components, utilities;
@import url("./layers/all.css");
}
/* WHY a full restatement rather than a patch: without @layer support the
block above is discarded in its entirety, so this branch is not an
override — it is the whole stylesheet for those engines. */
@supports not at-rule(@layer) {
@import url("./fallback/all.css");
}Frequently Asked Questions
Why does CSS.supports('@layer') not work?
Because CSS.supports evaluates declarations and selectors, and an at-rule is neither. Its one-argument form parses a supports condition made of property: value pairs and selector() tests; an at-rule name matches nothing in that grammar, so the call returns false in every engine — including ones with complete layer support. The result is indistinguishable from a genuine negative, which is what makes it a trap rather than merely a limitation.
Is runtime detection of @layer worth shipping?
For most audiences, no. Layers reached Baseline in March 2022, so the detection code, the fallback stylesheet and the branch they create usually cost more to maintain than the traffic they serve. It earns its place in two situations: a measurable share of visitors on pre-2022 engines, or a component that must choose between two behaviours rather than two stylings, where CSS alone cannot express the decision.
What happens in a browser that does not support @layer?
The engine does not recognise the at-rule, so it discards the rule together with everything inside its block. That is a complete loss of the layered styles rather than a graceful degradation — the page renders with whatever unlayered CSS remains, which for a fully migrated codebase is nothing at all. It is the reason a fallback must be a parallel stylesheet, and the reason generating it with the PostCSS polyfill beats maintaining it by hand.
Related
- Testing Cascade Layers Across Browsers — the wider testing strategy
- @layer Browser Support and Polyfills — the support matrix behind these decisions
- Progressive Enhancement for Older Safari — the feature-query approach applied to a specific engine
- Configuring the PostCSS Cascade Layers Polyfill — generating the fallback bundle rather than writing it
Up: Testing Layers Across Browsers → Browser Support, Compatibility & Migration