Articles /
Liquid Glass in the Browser: Refraction with CSS and SVG
Introduction
Apple introduced the Liquid Glass effect during WWDC 2025 in June—a stunning UI effect that makes interface elements appear to be made of curved, refractive glass. This article is a hands‑on exploration of how to recreate a similar effect on the web using CSS, SVG displacement maps, and physics-based refraction calculations.
Instead of chasing pixel‑perfect parity, we’ll approximate Liquid Glass, recreating the core refraction and a specular highlight, as a focused proof‑of‑concept you can extend.
We'll build up the effect from first principles, starting with how light bends when passing through different materials.
Chrome‑only demo
The interactive demo at the end currently works in Chrome only (due to SVG filters as backdrop‑filter).
You can still read the article and interact with the inline simulations in other browsers.
Understanding Refraction
Refraction is what happens when light changes direction as it passes from one material to another (like from air into glass). This bending occurs because light travels at different speeds through different materials.
The relationship between the incoming and outgoing light angles is described by Snell–Descartes law:
In the above interactive diagram, you can see that:
- When , the light ray passes straight through without bending.
- When , the ray bends toward the normal (the imaginary line perpendicular to the surface).
- When , the ray bends away from the normal, and depending on the angle of incidence, it may bend so much that it reflects back into the original medium instead of passing through. This is called Total Internal Reflection
- When incident ray is orthogonal to the surface, it passes straight through regardless of refractive indices.
Limitations in this project
To keep things focused we avoid complex branches of behavior by constraining the scenario:
- Ambient medium has refractive index of 1 (air).
- Use materials with refractive index > 1, and prefer 1.5 (glass).
- Only one refraction event (ignore any later exit / second refraction).
- Incident rays are always orthogonal to the background plane (no perspective).
- Objects are 2D shapes parallel to the background (no 3D, no perspective).
- No gap: objects touch the background plane.
- Two controls: bezel (edge transition) and thickness (height under bezel).
- Circle shapes only in this article. The same ideas extend to other shapes with a bit more preliminary geometry. Circles also let us form rounded rectangles by stretching the middle along the x or y axis.
Under these assumptions every ray we manipulate has a well-defined refracted direction via Snell's Law, and we simplify a lot our calculations.
Creating the Glass Surface
To create our glass effect, we need to define the shape of our virtual glass surface. Think of this like describing the cross-section of a lens or curved glass panel.
Surface Function
Our glass surface is described by a mathematical function that defines how thick the glass is at any point from its edge to the end of the bezel. This surface function takes a value between 0 (at the outer edge) and 1 (end of bezel, start of flat surface) and returns the height of the glass at that point.
const height = f(distanceFromSide);
From the height we can calculate the angle of incidence, which is the angle between the incoming ray and the normal to the surface at that point. The normal is simply the derivative of the height function at that point, rotated by -90 degrees:
const delta = 0.001; // Small value to approximate derivative
const y1 = f(distanceFromSide - delta);
const y2 = f(distanceFromSide + delta);
const derivative = (y2 - y1) / (2 * delta);
const normal = { x: -derivative, y: 1 }; // Derivative, rotated by -90 degrees
Equations
For this article, we will use four different height functions to demonstrate the effect of the surface shape on the refraction:
This convex circle surface uses a simple circular arc equation, creating a perfectly spherical dome shape. While simpler than the squircle approach, this creates more abrupt transitions between the curved bezel and flat surface, resulting in sharper refraction boundaries that can be more noticeable when stretched into non-circular shapes.
This convex surface uses a Squircle equation, that Apple commonly employs, which provides better smoothing between flat and curved surface transitions for more natural refraction effects. This approach allows objects to be stretched into rectangular shapes without creating harsh boundaries at the flat surface areas.
It makes the bezel optical size appear tinyer than its physical size, as the flatter areas near the edges contribute less to refraction.
The concave surface is the horizontal symmetry of the convex function, creating a bowl-like depression. This surface causes light rays to diverge outward, potentially displacing them beyond the glass boundaries, which can impact performance but creates interesting magnification effects at the edges.
The lip bezel combines both convex and concave characteristics, interpolated using Smootherstep. This creates a raised edge that transitions to a slight depression in the center.
The offset in the concave function is just to reduce the depth of the concave part, therefore reducing refraction outwards.
We could make the surface function more complex by adding more parameters, but these four already give a good idea of how the surface shape affects the refraction.
Simulation
Now let's see these surface functions in action through interactive ray tracing simulations. The following visualization demonstrates how light rays behave differently as they pass through each surface type, helping us understand the practical implications of our mathematical choices.
From the simulation, you can see that concave surfaces push rays outside the glass; convex surfaces keep them inside.
We want to avoid outside displacement because it requires sampling background beyond the object. Apple’s Liquid Glass appears to favor convex profiles (except the Switch, covered later).
The background arrow indicates displacement—how far a ray lands compared to where it would have landed without glass. Color encodes magnitude (longer → more purple).
Take a look at symmetry: rays at the same distance from the border share the same displacement magnitude on each side. Compute once, reuse around the bezel/object.
Displacement Vector Field
Now that calculated the displacement at a distance from border, let's calculate the displacement vector field for the entire glass surface.
The vector field describes at every position on the glass surface how much the light ray is displaced from its original position, and in which direction. In a circle, this displacement is always orthogonal to the border.
Pre-calculating the displacement magnitude
Because we saw that this displacement magnitude is symmetric around the bezel, we can pre-calculate it for a range of distances from the border, on a single radius.
This allows us to calculate everything in two dimensions once (x and z axis), on one "half-slice" of the object, and we will the rotate these pre-calculated displacements around the z-axis.
The actual number of samples we need to do on a radius is of 127 ray simulations, and is determined by the constraints of the SVG Displacement Map resolution. (See next section.)
Normalizing vectors
In the above diagram, the arrows are all scaled down for visibility, so they do not overlap. This is normalization, and is also useful from a technical standpoint.
To use these vectors in a displacement map, we need to normalize them. Normalization means scaling the vectors so that their maximum magnitude is 1, which allows us to represent them in a fixed range.
So we calculate the maximum displacement magnitude in our pre-calculated array:
const maximumDisplacement = Math.max(...displacementMagnitudes);
And we divide each vector's magnitude by this maximum:
displacementVector_normalized = {
angle: normalAtBorder,
magnitude: displacementMagnitude / maximumDisplacement, // Normalize the magnitude to be between 0 and 1
};
We store maximumDisplacement
as we will need it to re-scale the displacement map back to the actual magnitudes.
SVG Displacement Map
Now we need to translate our mathematical refraction calculations into something the browser can actually render. We'll use SVG displacement maps.
A displacement map is simply an image where each pixel's color tells the browser how far it should find the actual pixel value from its current position.
SVG's <feDisplacementMap />
encodes these pixels in a 32 bit RGBA image, where each channel represents a different axis of displacement.
It's up to the user to define which channel corresponds to which axis, but it is important to understand the constraint: Because each channel is 8 bits, the displacement is limited to a range of -128 to 127 pixels in each direction. (256 values possible in total). 128 is the neutral value, meaning no displacement.
SVG filters can only use images as displacement maps, so we need to convert our displacement vector field into an image format.
<svg colorInterpolationFilters="sRGB">
<filter id={id}>
<feImage
href={displacementMapDataUrl}
x={0}
y={0}
width={width}
height={height}
result="displacement_map"
/>
<feDisplacementMap
in="SourceGraphic"
in2="displacement_map"
scale={scale}
xChannelSelector="R" // Red Channel for displacement in X axis
yChannelSelector="G" // Green Channel for displacement in Y axis
/>
</filter>
</svg>
<feDisplacementMap />
uses the red channel for the X axis and the green channel for the Y axis. The blue and alpha channels are ignored.
Scale
The Red (X) and Green (Y) channels are 8‑bit values (0–255). Interpreted without any extra scaling, they map linearly to a normalized displacement in [−1, 1], with 128 as the neutral value (no displacement):
The scale
attribute of <feDisplacementMap />
multiplies this normalized amount:
Because our vectors are normalized using the maximum possible displacement (in pixels) as the unit, we can reuse that maximum directly as the filter’s scale
:
<feDisplacementMap
in="SourceGraphic"
in2="displacement_map"
scale={maximumDisplacement} // max displacement (px) → real pixel shift
xChannelSelector="R"
yChannelSelector="G"
/>
You can also animate scale
to fade the effect in/out—no need to recompute the map (useful for artistic control even if not physically exact).
Vector to Red-Green values
To convert our displacement vector field into a displacement map, we need to convert each vector into a color value. The red channel will represent the X component of the vector, and the green channel will represent the Y component.
We currently have polar coordinates (angle and magnitude) for each vector, so we need to convert them to Cartesian coordinates (X and Y) before mapping them to the red and green channels.
const x = Math.cos(angle) * magnitude;
const y = Math.sin(angle) * magnitude;
Because we normalised our vectors already, magnitude
here is between 0 and 1.
From here, we just remap the values to the range of 0 to 255 for the red and green channels:
const result = {
r: 128 + x * 127, // Red channel is the X component, remapped to 0-255
g: 128 + y * 127, // Green channel is the Y component, remapped to 0-255
b: 128, // Blue channel is ignored
a: 255, // Alpha channel is fully opaque
};
After converting every vector in the map to color value, we get an image that can be used as a displacement map in the SVG filter.
Playground
This playground applies the SVG displacement filter to a simple scene and lets you tweak surface shape, bezel width, glass thickness, and effect scale. Watch how these inputs change the refraction field, the generated displacement map, and the final rendering.
Surface
Controls
Ray Simulation
Displacement Map
Calculated Displacements
Preview
Specular Highlight
The final piece of our Liquid Glass effect is the specular highlight—those bright, shiny edges you see on real glass objects when light hits them at certain angles.
To achieve it, we define an arbitrary angle, and we adjust opacity of pixels of the border around the shape, based on how aligned are the this arbitrary angle, and the normal at the border.
Combining Refraction and Specular Highlight
In the final SVG filter, we combine both the displacement map for refraction and the specular highlight effect.
Both are loaded as separate <feImage />
elements, and then combined using <feBlend />
to overlay the highlight on top of the refracted image.
But this part is actually the most "creative" part of the effect, and it's just by tweaking the number of filters, and their parameters, that you can get a variety of different looks.
SVG Filter as backdrop-filter
This is the part where cross-browser compatibility ends. Only Chrome currently supports using SVG filters as backdrop-filter
, which is essential for applying the Liquid Glass effect to UI components:
.glass-panel {
backdrop-filter: url(#liquidGlassFilterId);
}
Note: The backdrop-filter dimensions does not adjust automatically to the element size, so you need to ensure that your filter images fit the size of your elements.
Now that we have all the pieces in place, we can create components that use this effect.
Bringing It All Together: Real UI Components
With our refraction math and displacement maps working, let's see how this translates to actual UI components you might use in an application.
Chrome‑specific demo
Chrome allows using SVG filters as backdrop-filter
, which
isn’t part of the CSS spec.
Above, we used regular filter
so it’s viewable in
Safari/Firefox.
The next components use backdrop-filter
, so they’re
Chrome‑only.
The goal won't be to create real components for production, but just to get a taste of how the effect looks in different UI elements.
Magnifying Glass
This component actually uses two displacement maps: one for the refraction on the sides, and one for the zooming, which has a stronger refraction effect.
It also plays with shadows and scaling to create a more dynamic, interactive effect.
Liquid Glass—Precision Lens
Drag the capsule to bend the page. This lens is a compact SVG displacement rig that refracts whatever sits beneath it.
The field comes from a rounded bezel profile; pixels are pushed along its gradient, then topped with a subtle specular bloom for depth.
Sweep across strong edges—high contrast makes the bend snap.
Searchbox
Switch
This uses a lip bezel, which makes the surface convex on the outside and concave in the middle. This makes the center slider zoomed out, while the edges refract the inside.
Slider
Slider allows you to see the current level through the glass, while the sides refract the background. It uses a convex bezel.
Music Player
This fake music player UI tries to mimic the look of Apple Music’s Liquid Glass panels, using a convex bezel and a subtle specular highlight.
It relies on the iTunes Search API to fetch album art and song details.
Conclusion
This proof‑of‑concept recreates the essence of Apple’s Liquid Glass through refraction (with a simple highlight). It’s convincing and extensible, but currently Chrome‑only because Chrome is the only browser that allows SVG filters as backdrop-filter
—there’s no practical cross‑browser fallback yet.
Treat it as an experiment rather than production code. It needs performance work, and dynamic sizes/shapes are expensive because any change (except the filter scale
) currently requires regenerating the displacement map.
The code isn’t published yet. If you’re interested, leave a comment and I’ll consider open‑sourcing it.