Liquid Glass Effect in CSS: iOS 26-Style Morphing Surfaces
Build iOS 26-style liquid glass surfaces in pure CSS - backdrop-filter, SVG turbulence distortion, and morphing animations, no canvas required.
What Liquid Glass Actually Is (and Why iOS 26 Made It Mainstream)
Apple shipped iOS 26 in June 2026 and within a week every designer on X was posting screenshots of the new Control Centre. That frosted, morphing, light-refracting surface they called 'Liquid Glass' isn't just a new blur radius - it's a layered technique that combines backdrop distortion with specular highlights and organic edge softening. It looks like water captured mid-pour over a pane of glass.
Glassmorphism has been around since roughly 2020. But liquid glass goes a step further: the surface itself appears to *warp* the content behind it, not just blur it. That's the distinction worth holding onto as you build this.
In practice, you're stacking three visual layers - a blurred background via backdrop-filter, a live distortion map using an SVG filter, and a specular highlight gradient overlay. Each layer does maybe 30% of the work. Together they read as physically plausible.
Worth noting: the effect is GPU-heavy. On mobile Safari you'll want to gate it behind a @media (prefers-reduced-motion: no-preference) check at minimum, and probably also a GPU capability check via JavaScript. Don't ship this naively to a 2019 mid-range Android and wonder why it janks.
The Core CSS Stack: backdrop-filter + SVG Turbulence
Start with backdrop-filter. You need at least blur(20px) to get the frosted base - anything under 12px reads as smudgy rather than glassy. The browser support story is fine as of 2026; even Firefox fully shipped it in version 103.
Here's the minimal structural CSS:
``css
.liquid-glass {
position: relative;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(24px) saturate(180%) brightness(1.1);
-webkit-backdrop-filter: blur(24px) saturate(180%) brightness(1.1);
border: 1px solid rgba(255, 255, 255, 0.18);
border-radius: 20px;
overflow: hidden;
}
`
saturate(180%)` is doing quiet work here - it punches up the colours bleeding through from behind, which sells the 'glass' metaphor. Drop it and you get frosted acrylic. Keep it and you get something closer to a prism.
The distortion part requires an inline SVG filter applied as a filter property (not backdrop-filter). This is where the 'liquid' comes from:
``html
<svg style="position:absolute;width:0;height:0">
<defs>
<filter id="liquid">
<feTurbulence
type="fractalNoise"
baseFrequency="0.015 0.012"
numOctaves="3"
seed="2"
result="noise"
/>
<feDisplacementMap
in="SourceGraphic"
in2="noise"
scale="8"
xChannelSelector="R"
yChannelSelector="G"
/>
</filter>
</defs>
</svg>
`
Apply it with .liquid-glass { filter: url(#liquid); }. The scale="8"` controls distortion strength - at 8px you get a subtle warp; push it to 20 and things get intentionally wobbly.
Honestly, the SVG filter approach is one of those CSS techniques that feels like cheating. You're describing noise math in XML and the browser's rendering engine does real physics-adjacent distortion for you. It's been available since Firefox 3.5. We just collectively forgot about it for a decade.
Specular Highlights: Making It Look Wet
The highlight is what separates 'frosted card' from 'liquid glass'. It's a gradient overlay positioned at the top-left of the element that mimics light catching the surface at an angle. On iOS 26, Apple animates this highlight in response to device tilt via gyroscope data. You can fake it with a mouse-tracking version.
Static version first:
``css
.liquid-glass::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(
135deg,
rgba(255, 255, 255, 0.35) 0%,
rgba(255, 255, 255, 0.05) 40%,
transparent 60%
);
border-radius: inherit;
pointer-events: none;
z-index: 1;
}
``
That 135-degree angle puts the light source top-left, which is conventional (users expect light from above-left by visual convention going back to skeuomorphic design in the early 2000s).
For the interactive version, track mousemove on the container and update CSS custom properties:
``js
const el = document.querySelector('.liquid-glass');
el.addEventListener('mousemove', (e) => {
const { left, top, width, height } = el.getBoundingClientRect();
const x = ((e.clientX - left) / width) * 100;
const y = ((e.clientY - top) / height) * 100;
el.style.setProperty('--light-x', ${x}%);
el.style.setProperty('--light-y', ${y}%);
});
`
Then in CSS: background: radial-gradient(circle at var(--light-x, 30%) var(--light-y, 20%), rgba(255,255,255,0.4), transparent 60%);`. The default fallback values mean it looks fine with no JS interaction.
One more thing - add a subtle box-shadow inside the element (using inset) to sell the depth. Something like box-shadow: inset 0 1px 0 rgba(255,255,255,0.3), inset 0 -1px 0 rgba(0,0,0,0.1), 0 20px 40px rgba(0,0,0,0.3). That top 1px white line is the edge catching light. It's a 1px value doing 40% of the realism.
Morphing the Shape: CSS Animations on border-radius
Liquid glass moves. The iOS 26 implementation subtly pulses the edges, which the human visual system reads as the glass responding to the content behind it - even though it isn't. It's a psychographic trick. Cheap to implement, expensive-looking result.
The secret is animating border-radius with asymmetric values:
``css
@keyframes liquidMorph {
0% { border-radius: 20px 28px 22px 18px / 22px 18px 26px 20px; }
25% { border-radius: 28px 16px 30px 22px / 18px 28px 20px 24px; }
50% { border-radius: 18px 30px 20px 26px / 28px 20px 22px 16px; }
75% { border-radius: 24px 20px 18px 28px / 20px 24px 18px 28px; }
100% { border-radius: 20px 28px 22px 18px / 22px 18px 26px 20px; }
}
.liquid-glass {
animation: liquidMorph 8s ease-in-out infinite;
}
`
CSS border-radius` accepts two sets of four values separated by a slash - horizontal and vertical radii independently. Animating both sets on different cycles produces organic shape variation that looks nothing like a rectangle transitioning to a circle. It looks alive.
Keep the animation duration long - 8 seconds minimum. Under 4 seconds it starts feeling like a loading spinner. The goal is something you almost don't notice consciously but would notice if it stopped. Subtle is the whole point.
If you want to go deeper on morphing approaches, the css-morphing-shapes article covers SVG path morphing as an alternative when you need more extreme shape changes than border-radius alone can deliver.
Putting It Together: Full Component Example
Here's everything combined into a self-contained component. Drop this into any React + Tailwind project or just use the vanilla version:
``tsx
// LiquidGlass.tsx
import { useRef, useEffect } from 'react';
export function LiquidGlass({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const handler = (e: MouseEvent) => {
const { left, top, width, height } = el.getBoundingClientRect();
el.style.setProperty('--lx', ${((e.clientX - left) / width) * 100}%);
el.style.setProperty('--ly', ${((e.clientY - top) / height) * 100}%);
};
el.addEventListener('mousemove', handler);
return () => el.removeEventListener('mousemove', handler);
}, []);
return (
<>
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
<defs>
<filter id="liquid-distort">
<feTurbulence type="fractalNoise" baseFrequency="0.015 0.012"
numOctaves="3" seed="2" result="noise" />
<feDisplacementMap in="SourceGraphic" in2="noise"
scale="8" xChannelSelector="R" yChannelSelector="G" />
</filter>
</defs>
</svg>
<div ref={ref} className="liquid-glass-root">
<div className="liquid-glass-inner">{children}</div>
</div>
</>
);
}
`
`css
.liquid-glass-root {
filter: url(#liquid-distort);
animation: liquidMorph 8s ease-in-out infinite;
}
.liquid-glass-inner {
position: relative;
background: rgba(255,255,255,0.08);
backdrop-filter: blur(24px) saturate(180%) brightness(1.1);
-webkit-backdrop-filter: blur(24px) saturate(180%) brightness(1.1);
border: 1px solid rgba(255,255,255,0.18);
border-radius: 20px;
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.3),
inset 0 -1px 0 rgba(0,0,0,0.1),
0 20px 40px rgba(0,0,0,0.3);
overflow: hidden;
}
.liquid-glass-inner::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(
circle at var(--lx, 30%) var(--ly, 20%),
rgba(255,255,255,0.4),
transparent 60%
);
pointer-events: none;
z-index: 1;
}
`
You'll want to wrap child content in a position: relative; z-index: 2` container so it renders above the highlight overlay.
Quick aside: applying filter: url(#liquid-distort) to the outer wrapper and backdrop-filter to the inner wrapper is intentional. If you apply both to the same element, the SVG filter will distort the backdrop-filter output in ways that look wrong. Keep them on separate DOM nodes.
For ready-made glass components you don't have to build yourself, check out the glassmorphism components collection - they're already performance-tested and accessible.
Performance, Accessibility, and When Not to Use This
Let's be direct: liquid glass is a flourish, not a layout primitive. You wouldn't use it for a data table or an error message. Where it shines is hero sections, modal overlays, notification cards, and anywhere you want the interface to feel physically present rather than flat.
Performance budget: backdrop-filter: blur(24px) triggers a compositing layer, which means your GPU is doing work on every frame it's visible. Multiple overlapping glass elements can tank frame rates on integrated graphics. Keep glass surfaces to 2-3 per viewport. If you need more, consider using a static blurred background image instead of live backdrop-filter for elements lower in the stacking context.
Accessibility matters here. The reduced-motion preference should disable the morphing animation entirely:
``css
@media (prefers-reduced-motion: reduce) {
.liquid-glass-root {
animation: none;
}
}
``
The light-tracking mousemove effect should also be disabled. Users with vestibular disorders can find motion-on-scroll and cursor-tracking effects disorienting - this isn't hypothetical, it's documented in WCAG 2.2 criterion 2.3.3.
Look, contrast is the other thing. A white text label on a semi-transparent glass surface over a light background is a WCAG failure waiting to happen. Always test your glass surfaces with actual content against your actual backgrounds, not just on that dramatic dark demo screenshot. The glassmorphism generator has a contrast checker built in - use it.
Beyond Basic Glass: Chromatic Aberration and Color Fringing
This is where you can push the effect from 'nice glassmorphism' to 'genuinely surprising'. Real glass separates light into its component wavelengths at the edges - that's chromatic aberration, and it's what makes thick glass objects look so visually rich.
You can fake it with multiple pseudo-elements or box shadows with different colour channels offset slightly:
``css
.liquid-glass-inner::after {
content: '';
position: absolute;
inset: -1px;
border-radius: inherit;
background: transparent;
box-shadow:
-1px 0 0 rgba(255, 100, 100, 0.15),
1px 0 0 rgba(100, 150, 255, 0.15);
pointer-events: none;
z-index: 0;
}
``
Subtle red fringing on the left edge, blue on the right. At 15% opacity it reads as physical realism. At 40% it reads as a design choice. Somewhere in between is where you want to live.
There's also a CSS filter: blur() trick where you create offset copies of the element's background at 1px displacement per RGB channel using SVG feColorMatrix - but that's genuinely complex and probably overkill unless you're building a design tool or a portfolio showpiece. The box-shadow approach gets you 80% of the visual result for 5% of the complexity.
If this kind of advanced filter work interests you, the backdrop-filter-css deep-dive covers the full filter function spec including drop-shadow, hue-rotate, and chaining multiple filters - worth bookmarking.
FAQ
backdrop-filter has full support across Chrome, Safari, Firefox, and Edge as of 2026. SVG feTurbulence distortion filters are similarly universal. No polyfill needed.
You're probably missing the saturate() and brightness() modifiers. A bare blur() looks like frosted acrylic - add saturate(160-200%) to get the glass colour refraction effect.
Tailwind's backdrop-blur-* and backdrop-saturate-* utilities handle the base layer. The SVG distortion filter and morphing animation need custom CSS - drop them in your globals.css.
border-radius animation doesn't trigger layout or paint - only compositing. It's one of the cheapest properties to animate. The SVG filter is the expensive part; keep it on a GPU-promoted layer with will-change: transform.