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-coloron 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-filterdoes not blur. NEVER assume the rule is malformed before checking parents :opacity < 1, anyfilter,mask,mix-blend-mode,clip-path, anotherbackdrop-filter,isolation: isolate, orcontain: paint / layout / stricton 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: reducebranch 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-filteron a single layer per stack. NEVER nest twobackdrop-filterelements ; the inner one establishes a backdrop-root and the outer one only sees the inner's solid surface. - ALWAYS animate
opacityortransform, neverbackdrop-filteritself. 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 :
opacityless than 1- Any
filtervalue other thannone mask,mask-image,mask-bordermix-blend-modeother thannormalclip-pathother thannone- Another
backdrop-filterother thannone isolation: isolatecontain: paint,contain: layout,contain: strictwill-changenaming any of the above properties
Stacking-context triggers (do NOT establish a backdrop-root but still affect layering) :
transformother thannone,perspectiveother thannoneposition: fixed,position: stickyz-indexon 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: activebehavior (which dropsbackdrop-filtereffects).[[frontend-perf-core-web-vitals-inp]]: compositor cost budget, avoid animating heavy filters on the critical paint path.
Reference Links
- references/methods.md : full filter-function surface, syntax, ancestor-trigger list, browser support table.
- references/examples.md : renderable HTML fragment with a sticky frosted header AND a broken-versus-fixed backdrop-root demo on the same page.
- references/anti-patterns.md : seven anti-patterns with symptom, root cause, and fix.
Authoritative sources
- MDN : backdrop-filter (verified 2026-05-19)
- MDN : CSS filter effects (verified 2026-05-19)
- W3C : Filter Effects Module Level 2 (verified 2026-05-19)
- MDN : @supports (verified 2026-05-19)
- MDN : prefers-reduced-transparency (verified 2026-05-19)