I've been building CSSTools.io — a collection of free CSS generator tools for developers. One of the most fun to build was the Glassmorphism Generator. This post is about the browser quirks I hit, the CSS math behind the effect, and why backdrop-filter is more nuanced than most tutorials let on.
What glassmorphism actually is (under the hood)
Most tutorials show you this and call it done:
.glass-card {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(16px);
}
That's the skeleton. But a real glassmorphism effect has five properties working together:
.glass-card {
background: rgba(255, 255, 255, 0.15); /* translucency */
backdrop-filter: blur(16px) saturate(180%); /* the actual glass */
-webkit-backdrop-filter: blur(16px) saturate(180%); /* Safari */
border: 1px solid rgba(255, 255, 255, 0.20); /* edge highlight */
border-radius: 20px; /* softness */
color: #ffffff;
}
The part most people miss is saturate(). Without it the blurred background looks washed out and grey. Cranking saturation to 150–200% is what gives glassmorphism that vivid, almost Studio-display quality.
The backdrop-filter problem I didn't expect
When I first built the preview in the tool, I had the glass card sitting directly over a solid dark background. The effect looked completely flat — just a slightly lighter box.
The reason: backdrop-filter needs something colorful behind it to blur. On a solid background there's nothing to blur through, so you just get a tinted box.
The fix was adding colored blob elements behind the card:
<div class="preview-area">
<div class="blob blob-1"></div> <!-- purple -->
<div class="blob blob-2"></div> <!-- pink -->
<div class="blob blob-3"></div> <!-- cyan -->
<div class="glass-card">...</div>
</div>
.preview-area {
position: relative;
overflow: hidden;
}
.blob {
position: absolute;
border-radius: 50%;
filter: blur(60px);
opacity: 0.7;
}
.blob-1 {
width: 260px; height: 260px;
background: #7c6fff;
top: -60px; left: -60px;
}
Now the backdrop-filter on the card blurs those blobs — and suddenly the effect looks exactly like Apple's macOS design.
The CSS math behind opacity and border
In the generator I expose five sliders:
-
Blur — maps directly to
blur(Npx)inbackdrop-filter -
Opacity — maps to the alpha in
rgba(), but divided by 100:rgba(r, g, b, opacity/100) -
Border — maps to the border's alpha:
rgba(255, 255, 255, border/100) -
Radius — maps directly to
border-radius: Npx -
Saturation — maps to
saturate(N%)inbackdrop-filter
The JS that generates the CSS output is straightforward:
function updateGlass() {
const blur = parseInt(document.getElementById('glassBlur').value);
const opacity = parseInt(document.getElementById('glassOpacity').value);
const border = parseInt(document.getElementById('glassBorder').value);
const radius = parseInt(document.getElementById('glassRadius').value);
const sat = parseInt(document.getElementById('glassSat').value);
const alpha = opacity / 100;
const borderAlpha = border / 100;
// Hex to RGB for rgba()
const rgb = hexToRgb(glassBgColor);
card.style.background = `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`;
card.style.backdropFilter = `blur(${blur}px) saturate(${sat}%)`;
card.style.webkitBackdropFilter = `blur(${blur}px) saturate(${sat}%)`;
card.style.border = `1px solid rgba(255, 255, 255, ${borderAlpha})`;
card.style.borderRadius = `${radius}px`;
// Output the CSS
codeBlock.textContent = `.glass-card {
background: rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha});
backdrop-filter: blur(${blur}px) saturate(${sat}%);
-webkit-backdrop-filter: blur(${blur}px) saturate(${sat}%);
border: 1px solid rgba(255, 255, 255, ${borderAlpha});
border-radius: ${radius}px;
color: ${glassTextColor};
}`;
}
One thing that tripped me up early: I was using element.style.backdropFilter but Safari requires element.style.webkitBackdropFilter as a separate property — not as a fallback inside a string, but as a distinct JS property assignment. Easy to miss when you're setting styles programmatically.
Browser support is better than you think
A common myth is that backdrop-filter has poor browser support. The reality in 2026:
| Browser | Support |
|---|---|
| Chrome | ✅ 76+ |
| Safari | ✅ 9+ (with -webkit-) |
| Firefox | ✅ 103+ |
| Edge | ✅ 79+ |
The main gotcha is Firefox requires layout.css.backdrop-filter.enabled to be true in about:config for versions before 103. Anyone on a current Firefox is fine.
The other gotcha: backdrop-filter has no effect if the element or any ancestor has overflow: hidden with a non-default z-index in some browsers. I hit this on Safari when I wrapped the card in a container with overflow: hidden — the blur stopped working entirely. The fix was removing overflow: hidden from the parent and using clip-path instead when I needed to constrain the visible area.
Adding shareable URLs
One feature I'm genuinely happy with is stateful URLs. Every time you adjust a slider the URL updates:
csstools.io/glassmorphism?blur=16&opacity=15&border=20&radius=20&sat=180&bg=1a1a2e&text=ffffff
The implementation reads URL params before the first render so the tool loads with exactly the shared state:
(function loadFromURL() {
const p = new URLSearchParams(window.location.search);
if (!p.has('blur') && !p.has('opacity')) return;
if (p.has('blur')) document.getElementById('glassBlur').value = p.get('blur');
if (p.has('opacity')) document.getElementById('glassOpacity').value = p.get('opacity');
if (p.has('border')) document.getElementById('glassBorder').value = p.get('border');
if (p.has('radius')) document.getElementById('glassRadius').value = p.get('radius');
if (p.has('sat')) document.getElementById('glassSat').value = p.get('sat');
if (p.has('bg')) {
glassBgColor = '#' + p.get('bg');
// sync color picker UI...
}
})();
// Runs BEFORE updateGlass() so the first render uses URL values
updateGlass();
The key detail: loadFromURL must run before the initial render call, not after. If you call it after, the render fires with defaults and then history.replaceState in syncURL overwrites your URL params before loadFromURL reads them. Cost me two hours.
Values that actually look good
After tuning hundreds of combinations in the tool, here are the ranges that work in practice:
Subtle card (dashboard UI):
backdrop-filter: blur(10px) saturate(150%);
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.12);
Strong frosted glass (hero card):
backdrop-filter: blur(20px) saturate(200%);
background: rgba(255, 255, 255, 0.18);
border: 1px solid rgba(255, 255, 255, 0.25);
Dark glass (modal on light background):
backdrop-filter: blur(14px) saturate(160%);
background: rgba(0, 0, 0, 0.25);
border: 1px solid rgba(255, 255, 255, 0.08);
Blur above 25px starts hurting performance on mobile. Opacity above 30% loses the glass look. Saturation below 130% looks grey and lifeless.
Try it yourself
If you want to experiment without writing any CSS, the Glassmorphism Generator on CSSTools.io lets you dial in all five values with live preview and generates the complete cross-browser CSS to copy. You can also share your exact configuration via URL — useful for handing off effects to a teammate.
The full site has 18 other CSS tools — gradients, clip-path, box shadows, animations and more — all free, no signup.
Built with vanilla HTML, CSS and JS — no frameworks, no build step. The whole site loads in under 100ms.
Tags: css webdev javascript frontend tutorial
Cover image suggestion: A dark background with a colorful glassmorphism card showing the CSS code output — the tool itself makes a good screenshot.
Top comments (0)