Progressive Enhancement for Older Safari

A layered design system looks broken on an aging iPad because Safari 15.2 threw the entire @layer block away — this guide fixes that with no polyfill, using @supports at-rule(@layer) and deliberate fallback ordering. It is part of Browser Support & Polyfills within Browser Support, Compatibility & Migration.

Prerequisites

Before applying this pattern, you should understand:

  • That an unsupported engine discards a whole @layer block as an unknown at-rule — the behaviour explained in @layer browser support and polyfills.
  • That unlayered author styles outrank every named layer, a rule from understanding layer declaration order. This is why fallback ordering has to be deliberate.
  • When to prefer this over a build transform: the PostCSS cascade layers polyfill reproduces exact precedence but ships to everyone and raises specificity; this pattern trades exactness for zero modern-browser cost.
  • Tooling required: nothing beyond your normal CSS pipeline. This is a pure-CSS technique — no plugin, no build step.

Step-by-step procedure

Step 1 — Write the unlayered fallback first

For every property you intend to enhance with layers, author a plain, unlayered declaration. This is the only rule an old Safari will parse.

/* Fallback: no @layer, so Safari 15.2 parses and applies it.
   Keep the selector simple and the specificity low. */
.card {
  border: 1px solid #ccc;
  padding: 1rem;
}

What this does: Establishes a baseline appearance that renders in every engine ever shipped. Old Safari will use exactly these values; modern engines will use them only until the enhancement overrides them.


Step 2 — Gate the layered version behind @supports at-rule(@layer)

Wrap the layered rules in a feature query that tests for the @layer at-rule itself.

/* Only engines that understand the @layer at-rule enter this block.
   Safari < 15.4 does not, so it skips everything inside. */
@supports at-rule(@layer) {
  @layer components {
    .card {
      border: 1px solid var(--color-border);
      padding: var(--space-md);
      box-shadow: var(--shadow-sm);
    }
  }
}

What this does: Confines the enhanced, token-driven styling to browsers that support layers. The at-rule() form of @supports postdates @layer, so the oldest engines ignore the entire query and never see the layered rule.


Step 3 — Order fallback before enhancement and keep specificity flat

Source order and specificity both matter here. Place the fallback above the @supports block, and keep both selectors at the same low specificity so the cascade — not a weight accident — decides the modern-browser outcome.

/* 1. Fallback, unlayered, specificity (0,1,0). */
.card { border: 1px solid #ccc; }

/* 2. Enhancement, gated. In a modern engine this @layer rule and the
      unlayered fallback above both match .card at (0,1,0). Because the
      layered rule is the ENHANCEMENT we want to win, we rely on the
      @supports gate keeping the two apart conceptually — see the note. */
@supports at-rule(@layer) {
  @layer components { .card { border: 1px solid var(--color-border); } }
}

What this does: In old Safari, only rule 1 exists, so it wins by default. In a modern engine both rules parse — and here is the crucial detail: an unlayered rule outranks a layered one, so to guarantee the enhancement wins you must ensure the fallback does not also target the enhanced element at equal-or-lower specificity in a way that beats it. The robust fix is Step 4.


Step 4 — Scope the fallback so it cannot beat the enhancement

Give the fallback a scope that old Safari honours but that the layered version supersedes cleanly. The simplest reliable technique: put the fallback itself inside its own guarded context, or keep fallback and enhancement targeting the same selector but let the enhancement live in a layer and re-declare the value so modern engines pick it — using a wrapper the old engine ignores.

/* Robust pattern: the fallback is the DEFAULT; the enhancement re-states
   every property it wants to change. Because modern browsers read BOTH,
   and unlayered beats layered, we invert the usual layering here:
   put the ENHANCED values unlayered inside @supports, so they win. */

.card { border: 1px solid #ccc; padding: 1rem; }          /* old + new baseline */

@supports at-rule(@layer) {
  /* Declare the layer order for the rest of your architecture… */
  @layer reset, base, themes, components, utilities, overrides;

  /* …but express THIS enhancement unlayered inside the gate so it reliably
     beats the baseline above in modern engines (unlayered > layered),
     while old Safari never enters this block at all. */
  .card { border: 1px solid var(--color-border); box-shadow: var(--shadow-sm); }
}

What this does: Sidesteps the “unlayered beats layered” trap. The baseline .card renders in old Safari; in modern engines the @supports block’s later, equally-specific .card wins by source order and adds the token-driven enhancement. Your broader @layer architecture is still declared for everything else.


Step 5 — Verify both engines

Confirm the two paths independently.

Old Safari (real device or older WebKit build). Open Web Inspector → Elements → Styles on a .card. You should see only the plain .card { border: 1px solid #ccc } rule; the @supports block should not appear at all, because the engine could not parse at-rule(@layer).

Modern engine (Chrome / current Safari). Inspect the same .card. The @supports-gated declaration should win, showing the var(--color-border) border and the box-shadow. In Chrome’s Cascade Layers view, confirm your architecture’s other rules still group under their named layers.


Troubleshooting

The enhancement never applies, even in modern browsers : The browser may not support the at-rule() form of @supports even though it supports @layer — the at-rule() syntax is itself relatively new. Test with @supports at-rule(@layer) in a current release; if you must support engines in the gap, fall back to letting the unknown-at-rule behaviour degrade the layer block directly, without the @supports wrapper.

Old Safari shows the enhanced styling anyway : A property you enhanced was left outside the @supports block as a second unlayered rule that old Safari can parse. Move every enhanced declaration inside the gate, and keep only the intended baseline values unlayered and outside it.

The baseline flashes then disappears in modern browsers : This is normal cascade resolution, not a bug, if it only happens at parse time. If it persists visually, the enhancement’s selector is less specific than the baseline’s — equalise the selectors (both .card) so source order inside the gate decides.

@supports at-rule(@layer) reports false in a browser you expected to pass : Verify you are testing the at-rule form, not @supports (selector(...)) or a property query. Only at-rule(@layer) tests @layer support; the other forms are unrelated proxies and can mislead.

A !important in the fallback overrides the enhancement : !important on an unlayered fallback beats normal layered and unlayered rules alike. Remove it — per the role of !important in layers the baseline should never need it, and it defeats the whole enhancement.


Complete working example

A self-contained stylesheet that renders acceptably in Safari 15.2 and fully in modern engines, with no polyfill and no build step.

/* ============================================================
   card.css — progressive enhancement for Safari < 15.4
   ============================================================ */

/* ── Baseline: unlayered, parsed by EVERY browser ───────────── */
.card {
  border: 1px solid #ccc;
  border-radius: 4px;
  padding: 1rem;
  background: #fff;
  color: #111;
}
.card__title { font-weight: 600; }

/* ── Enhancement: only engines that parse @layer enter here ──── */
@supports at-rule(@layer) {
  /* Declare the full architecture for everything else on the page. */
  @layer reset, base, themes, components, utilities, overrides;

  @layer base {
    :root {
      --color-border: #d4d4d8;
      --color-surface: #ffffff;
      --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.08);
      --space-md: 1rem;
    }
  }

  /* Enhanced .card stated UNLAYERED inside the gate, so it reliably beats
     the baseline above in modern engines (unlayered outranks layered),
     while Safari 15.2 never reaches this block. */
  .card {
    border: 1px solid var(--color-border);
    background: var(--color-surface);
    padding: var(--space-md);
    box-shadow: var(--shadow-sm);
  }

  /* Component internals that only modern engines need can be fully layered. */
  @layer components {
    :where(.card__title) { letter-spacing: -0.01em; }
  }
}

Load this in Safari 15.2 and the card is a plain bordered box; load it in any 2022-or-later engine and it gains tokens, a surface colour, and a shadow — with no JavaScript and nothing shipped to modern browsers beyond the CSS they already parse.


FAQ

Why use @supports at-rule(@layer) instead of just relying on unknown at-rule fallback?

An old browser already discards a @layer block on its own, so a bare fallback plus a layer block does degrade. But @supports at-rule(@layer) makes the intent explicit and lets you gate more than just the layer block — for example, enhanced properties that should only apply alongside layers. It also prevents a subtle trap: if your fallback and your layered rule live in the same file without a gate, a modern browser reads both, and unlayered author styles outrank named layers, so the fallback would beat the enhancement.

Does the unlayered fallback beat my @layer rule in modern browsers?

It would, if you left it ungated. Unlayered author styles sit above every named layer in the cascade, so a plain .card fallback beats the same rule inside @layer components. That is exactly why the enhanced values go inside @supports at-rule(@layer) — and, in the robust pattern, stated unlayered within the gate so they win by source order on modern engines while old Safari never enters the block.

Is this approach better than the PostCSS polyfill?

It is better when you only need a graceful, acceptable fallback for old Safari rather than pixel-exact layer precedence, because it ships no extra bytes to modern browsers and does not inflate specificity globally. Choose the PostCSS polyfill instead when a large legacy audience depends on the precise layered outcome. For most teams in 2026 the pre-15.4 audience is tiny, so the lighter @supports approach is the better default.


Up: Browser Support & PolyfillsBrowser Support, Compatibility & Migration