Home / Snippets / UI /

Fallback UI pattern

Use @supports to progressively enhance components — show a solid fallback for older browsers while unlocking advanced CSS features for modern ones.

Card with @supports progressive enhancement
@supports (backdrop-filter: blur(1px))
Glass card
Solid opaque background on browsers without backdrop-filter. Frosted glass effect where backdrop-filter is supported.
Uses @supports
uino-jsprogressive-enhancement

Quick implementation

/* Step 1: Define the solid fallback — always applies */
.glass-card {
  background: oklch(0.22 0.03 260);
  border: 1px solid oklch(0.32 0.04 265 / 0.6);
  border-radius: 0.5rem;
  padding: 1.5rem;
  color: oklch(0.93 0.01 260);
}

/* Step 2: Enhance only when backdrop-filter is supported */
@supports (backdrop-filter: blur(1px)) {
  .glass-card {
    background: oklch(0.2 0.02 260 / 0.5);
    backdrop-filter: blur(8px) saturate(1.4);
    border-color: oklch(0.5 0.08 265 / 0.4);
  }
}

/* Another common use case: CSS grid fallback */
.layout {
  display: flex;      /* fallback */
  flex-wrap: wrap;
  gap: 1rem;
}

@supports (display: grid) {
  .layout {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  }
}

Prompt this to your LLM

Includes role, constraints, use-case examples, and edge cases to handle.

You are a senior frontend engineer teaching progressive CSS
enhancement using the @supports at-rule.

Goal: Show how to write CSS that provides a solid, usable fallback
for all browsers while layering enhanced styles on top for browsers
that support a specific CSS feature — demonstrated with a glass-card
component using backdrop-filter and a layout pattern using CSS grid.

Technical constraints:
- Always define the solid fallback first, outside any @supports block.
  The fallback must be completely usable on its own.
- Use @supports (backdrop-filter: blur(1px)) to detect support and
  apply the frosted glass effect. The test uses a minimal valid
  value, not the full desired value.
- Inside the @supports block, override only the properties that
  change — do not repeat the entire rule set.
- Use oklch() for all color values — no hex or rgba.
- No JavaScript required.

Show both supported and unsupported states in comments:
  /* Fallback: solid opaque card */
  /* Enhanced: frosted glass card */

Common use cases to demonstrate:
1. backdrop-filter glass card: solid background fallback,
   semi-transparent + blur enhancement.
2. CSS grid layout: flex/wrap fallback, grid enhancement.
3. @supports not() — show how to write styles that ONLY apply
   when a feature is NOT supported, for precise fallback targeting.

Edge cases to handle:
- @supports checks syntax support, not rendering quality — a browser
  can support backdrop-filter but produce poor results on low-end
  hardware. Note this limitation.
- The cascade order matters: the @supports block must come AFTER
  the fallback in the stylesheet so it wins specificity when matched.
- Avoid testing for properties that are universally supported
  (like display: flex) — only use @supports for genuinely
  uncertain feature support.

Return CSS only.

What @supports does

The @supports at-rule is CSS's feature query mechanism. It tests whether the browser understands a specific CSS property-value pair and conditionally applies the enclosed rules only if the test passes. Unlike media queries which test environmental conditions (viewport width, color scheme), @supports tests the browser's own CSS parsing capability.

The test always takes the form @supports (property: value). The value in the test doesn't need to be the full value you intend to use — it just needs to be a syntactically valid value for that property that the browser would accept. For backdrop-filter, testing blur(1px) is sufficient to confirm support before applying blur(8px) saturate(1.4).

You can combine tests with and, or, and not operators: @supports (display: grid) and (gap: 1rem) checks that both properties are supported. @supports not (backdrop-filter: blur(1px)) targets browsers that lack the feature — useful for applying a fallback-specific style that would be overridden by the enhanced version.

Writing good fallbacks

The golden rule: write the fallback first, without any @supports block. The fallback must be fully usable on its own — not a broken or visually degraded experience, but a complete, working design. Only then add the @supports block to layer the enhancement on top.

For the glass card example: the fallback is a solid opaque card with a slightly elevated background color. It looks good, reads well, and communicates the card affordance clearly. The @supports block then adds the frosted glass treatment — same structure, same readability, but with a more premium visual effect for browsers that support it.

This approach means the fallback is tested in every browser, not just old ones. If your @supports test fails in a browser you didn't expect, users still get a working UI. The enhancement is genuinely additive, not a band-aid over a broken fallback.

Common use cases

The most common @supports use cases are: backdrop-filter for glass morphism effects (still not supported in all environments), CSS subgrid for nested grid alignment (recently landed in all browsers but older versions lack it), @layer cascade layers (no longer needed as support is now universal), and newer color functions like oklch() or color-mix().

For the grid layout fallback pattern: a display: flex; flex-wrap: wrap fallback gives a workable wrapping layout in older browsers. Inside @supports (display: grid), switch to a proper auto-responsive grid with minmax() and auto-fit. Since grid support is now universal in modern browsers, this particular pattern is mainly relevant for very old browser support requirements.

The cascade order matters

The @supports block must appear after the fallback rules in the stylesheet. CSS resolves conflicts by taking the last matching rule (assuming equal specificity). If you write the enhanced rules before the fallback, the fallback will overwrite the enhancement in supporting browsers — exactly the opposite of what you want.

The same specificity rules apply inside @supports: if your fallback rule and your enhanced rule target the same selector, they have the same specificity and the last one wins. This is why the enhanced @supports block goes at the end — it overwrites only the properties it declares, leaving the rest of the fallback rule intact.

One nuance: @supports blocks do not change specificity. A rule inside @supports has exactly the same specificity as the same rule outside it. The cascade still applies normally — only the condition for including the rule changes.