SuperAPI AI Directory

frontend-visual-glassmorphism-backdrop

Frontend & websites

View repository

Updated 10-15 22:18

About this skill

Frontend Visual : Glassmorphism + backdrop-filter

This skill is the operational reference for shipping CSS backdrop-filter-based glassmorphism in evergreen-2026 browsers. It covers the property syntax, the partially-transparent background requirement, the full set of backdrop-root triggers (the single most common reason a backdrop-filter silently does nothing), contrast preservation against the blurred backdrop, the mobile performance budget, the @supports gate for pre-Baseline-2024 browsers, and the prefers-reduced-transparency opt-out. The skill does NOT cover gradients (see [[frontend-visual-gradients]]), micro-interaction motion (see [[frontend-visual-micro-interactions]]), light-dark() theming, or React component wrappers.

Quick Reference

Floor rules

  • ALWAYS set a partially-transparent background-color on the glass element. NEVER omit ; without a translucent surface the filter has nothing to blur and the element appears solid.
  • ALWAYS audit the ancestor chain for backdrop-root triggers when a backdrop-filter does not blur. NEVER assume the rule is malformed before checking parents : opacity < 1, any filter, mask, mix-blend-mode, clip-path, another backdrop-filter, isolation: isolate, or contain: paint / layout / strict on any ancestor breaks the effect.
  • ALWAYS verify text-on-glass meets WCAG 1 4 3 (4 5 to 1 normal, 3 to 1 large) against the EFFECTIVE backdrop (the blurred + tinted result), not against the declared token. NEVER ship body text on a translucent surface without measuring contrast in the live page.
  • ALWAYS gate on @supports (backdrop-filter: blur(1px)) AND provide a solid-color fallback. NEVER ship a glass surface that becomes transparent on browsers below Baseline 2024.
  • ALWAYS provide a prefers-reduced-transparency: reduce branch that replaces the glass with a solid surface and drops the blur. NEVER ignore the user signal.
  • ALWAYS budget blur radius for mobile : 8 to 16 px max for animated or frequently-painted surfaces. NEVER set blur(40px) on a sticky element above a scrolling photograph on mid-range mobile.
  • ALWAYS keep backdrop-filter on a single layer per stack. NEVER nest two backdrop-filter elements ; the inner one establishes a backdrop-root and the outer one only sees the inner's solid surface.
  • ALWAYS animate opacity or transform, never backdrop-filter itself. Animating the filter triggers a per-frame backdrop re-sample (paint storm).

Decision tree 1 : Glass versus solid + opacity ?

Need to see content THROUGH the surface (blurred or saturated) ?
  -> Glass. `backdrop-filter: blur(Npx) saturate(180%)` on the element +
     translucent background-color. The filter acts on what is BEHIND the element.

Need to make the element itself semi-transparent (the element AND its descendants
fade together) ?
  -> Solid + opacity. `opacity: 0.7` on the element. NOTE : opacity less than 1
     establishes a stacking context AND a backdrop-root, so any descendant
     `backdrop-filter` will break. Decide which behavior you want.

Need to tint the underlying content without blurring ?
  -> Solid color with low alpha. `background: rgb(0 0 0 / 0.4)`. No filter cost.

Need text-readable surface over arbitrary user content (photo, video, ad) ?
  -> Solid + slight tint. The blurred backdrop is unpredictable ; contrast
     cannot be guaranteed without measurement.

Decision tree 2 : Backdrop-filter not blurring ?

Is the element's background-color fully opaque (alpha 1) ?
  -> Reduce alpha. `background: oklch(0.99 0 0 / 0.7)` or `rgb(255 255 255 / 0.6)`.

Does the element have NO background at all ?
  -> Set one. `backdrop-filter` only applies to the area covered by the
     element's painted box ; without a background-color, that area is empty.

Does any ancestor (any parent up to the root) have one of :
  opacity < 1, filter, mask, mask-image, mix-blend-mode, clip-path,
  another backdrop-filter, isolation: isolate, contain: paint / layout / strict,
  will-change for any of the above ?
  -> That ancestor became a backdrop-root. The filter only sees content INSIDE
     that root. Either remove the property or restructure the DOM so the glass
     is a sibling of the content it wants to blur.

Is the browser pre-Baseline-2024 ?
  -> Gate with @supports (backdrop-filter: blur(1px)) and provide a solid fallback.

Is it Safari only and the unprefixed property is silently dropped in some
older nested context ?
  -> Add -webkit-backdrop-filter alongside backdrop-filter. Harmless.

Decision tree 3 : What blur radius for what content ?

Sharp text or thin graphics behind the glass (UI screenshot, dashboard) ?
  -> 8 to 12 px. Higher radii merge readable details ; the surface still feels
     like glass without obscuring the structural shape.

Photograph behind a sticky header ?
  -> 12 to 20 px combined with saturate(140%) to 180%. The saturation lift
     compensates for blur-induced colour washing.

Animated background (video, gradient mesh) ?
  -> 16 to 24 px on desktop, 8 to 16 px on mobile. Pair with
     `transform: translateZ(0)` to keep the layer composited.

Modal overlay over the entire app ?
  -> 24 to 40 px. The modal is short-lived and full-screen, so the GPU cost
     is paid once. Skip the blur entirely on mobile in
     `prefers-reduced-transparency` or low-power mode.

Decision tree 4 : Mobile fallback strategy ?

Target device is high-end (Apple A-series, recent Pixel / Galaxy) ?
  -> Full blur (16 to 24 px) acceptable. Test on real device.

Target includes mid-range Android (older Snapdragon, MediaTek) ?
  -> Cap blur at 8 to 12 px AND avoid blur on animated / scrolled surfaces.
     Consider intersection-toggle : enable blur only when the element is in
     view and not currently scrolling.

User set "Reduce Transparency" at the OS level ?
  -> Inside @media (prefers-reduced-transparency: reduce), set
     backdrop-filter: none and a solid background-color. Honor the signal.

Browser below Baseline 2024 ?
  -> @supports gate. Fall back to a solid background-color with a slight
     tint to suggest layering.

Patterns

Pattern : Minimal frosted card

.glass {
  background: oklch(0.99 0 0 / 0.6);
  backdrop-filter: blur(12px) saturate(180%);
  -webkit-backdrop-filter: blur(12px) saturate(180%); /* harmless legacy */
  border: 1px solid oklch(0.99 0 0 / 0.3);
  border-radius: 12px;
}

@supports not (backdrop-filter: blur(1px)) {
  .glass {
    background: oklch(0.99 0 0 / 0.95);
  }
}

@media (prefers-reduced-transparency: reduce) {
  .glass {
    backdrop-filter: none;
    background: oklch(0.99 0 0);
  }
}

Three layers : the primary rule, the @supports graceful fallback, and the prefers-reduced-transparency opt-out. Ship all three.

Pattern : Sticky frosted header

.header {
  position: sticky;
  top: 0;
  z-index: 10;
  height: 64px;
  background: oklch(0.99 0 0 / 0.7);
  backdrop-filter: blur(16px) saturate(160%);
  border-bottom: 1px solid oklch(0.85 0 0 / 0.3);
}

position: sticky itself does NOT establish a backdrop-root, but verify the scroll container does not have one of the trigger properties.

Pattern : Glass card OVER content (correct DOM)

<body>
  <main class="content">...</main>
  <aside class="glass-panel">Content inside is blurred BEHIND this panel.</aside>
</body>
.glass-panel {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  background: oklch(0.99 0 0 / 0.6);
  backdrop-filter: blur(20px);
}

The glass element must NOT be a descendant of the content it wants to blur. If it is, that content is inside the same stacking context and may be excluded from the backdrop or trigger a backdrop-root.

Pattern : Backdrop-root pitfall (broken) + fix

<!-- BROKEN -->
<div style="opacity: 0.95">
  <div class="glass">backdrop-filter is silently dropped</div>
</div>

<!-- FIXED -->
<div style="background: oklch(0.99 0 0 / 0.95)">
  <div class="glass">backdrop-filter now works</div>
</div>

opacity: 0.95 on the parent establishes a backdrop-root. Replace with a translucent background-color (which does NOT establish a backdrop-root) when the visual intent was a faded surface.

Pattern : Contrast-preserving text on glass

.glass {
  background: oklch(0.99 0 0 / 0.6);
  backdrop-filter: blur(14px) saturate(180%) brightness(110%);
}

.glass h2 {
  color: oklch(0.20 0 0); /* near-black ; ratio against the lifted backdrop */
  text-shadow: 0 0 8px oklch(0.99 0 0 / 0.6); /* defensive halo */
}

brightness(110%) lifts the underlying backdrop so dark text retains contrast. Defensive text-shadow is a halo that survives a dark-image background ; verify the rendered ratio meets 4 5 to 1 against the typical case.

Pattern : Mobile-aware blur

.glass {
  background: oklch(0.99 0 0 / 0.7);
  backdrop-filter: blur(12px) saturate(160%);
}

@media (max-width: 768px) {
  .glass { backdrop-filter: blur(8px) saturate(140%); }
}

@media (prefers-reduced-transparency: reduce), (update: slow) {
  .glass {
    backdrop-filter: none;
    background: oklch(0.99 0 0);
  }
}

(update: slow) matches low-refresh-rate displays where compositor cost is most visible.

Pattern : Avoid animating backdrop-filter

/* WRONG : per-frame backdrop re-sample */
.glass { transition: backdrop-filter 300ms; }
.glass:hover { backdrop-filter: blur(24px); }

/* RIGHT : animate an overlay's opacity, keep the filter static */
.glass { backdrop-filter: blur(16px); }
.glass-overlay {
  position: absolute; inset: 0;
  background: oklch(0.99 0 0 / 0);
  transition: background 200ms;
}
.glass:hover .glass-overlay {
  background: oklch(0.99 0 0 / 0.2);
}

Backdrop-root triggers (full list)

Any of the following on ANY ancestor of the backdrop-filter element creates a backdrop-root and the filter cannot see beyond it :

  • opacity less than 1
  • Any filter value other than none
  • mask, mask-image, mask-border
  • mix-blend-mode other than normal
  • clip-path other than none
  • Another backdrop-filter other than none
  • isolation: isolate
  • contain: paint, contain: layout, contain: strict
  • will-change naming any of the above properties

Stacking-context triggers (do NOT establish a backdrop-root but still affect layering) :

  • transform other than none, perspective other than none
  • position: fixed, position: sticky
  • z-index on a positioned element

Source : MDN : backdrop-filter (verified 2026-05-19) and vooronderzoek §9 (verified 2026-05-19).

Baseline status

Surface Status Notes
backdrop-filter Newly Available September 2024 Baseline 2024. Treat as production-safe in evergreen-2026 ; gate older browsers via @supports.
-webkit-backdrop-filter Legacy prefix No longer required as of 2024 ; harmless to ship for older Safari contexts.
prefers-reduced-transparency Limited Availability Experimental ; ship the rule anyway for forward compatibility.
@supports (backdrop-filter: blur(1px)) Baseline Widely Available Use to gate solid-color fallback.

Source : MDN : backdrop-filter (verified 2026-05-19).

Cross-references

  • [[frontend-syntax-css-color-modern]] : oklch() for tinted translucent backgrounds, light-dark() for theme-aware glass.
  • [[frontend-visual-gradients]] : layered gradients as a non-blur alternative for "glass over photo" surfaces.
  • [[frontend-visual-micro-interactions]] : hover and focus transitions for interactive glass elements.
  • [[frontend-a11y-motion-contrast-wcag22]] : prefers-reduced-transparency, WCAG 1 4 3 contrast measurement, forced-colors: active behavior (which drops backdrop-filter effects).
  • [[frontend-perf-core-web-vitals-inp]] : compositor cost budget, avoid animating heavy filters on the critical paint path.

Reference Links

Authoritative sources