Liquid Glass in the Browser Refraction with CSS and SVG
2025
SEP
04
Photo by Martin Martz on Unsplash
照片由 Martin Martz 拍摄,来自 Unsplash。
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.
苹果公司在 2025 年 6 月的 WWDC 开发者大会上推出了 Liquid Glass 效果 ——一种令人惊艳的 UI 特效,它能让界面元素看起来像是由弯曲的折射玻璃制成。本文将通过实践操作,探索如何使用 CSS、SVG 位移贴图和基于物理的折射计算,在网页上实现类似的效果。
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 仅限 Chrome 浏览器的演示
The interactive demo at the end currently works in Chrome only (due to SVG filters as backdrop‑filter).
结尾处的交互式演示目前仅在 Chrome 浏览器中有效(因为使用了 SVG 滤镜作为背景滤镜)。
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:
入射光角度和出射光角度之间的关系可以用 斯涅尔-笛卡尔定律 来描述:
$n_1 \sin(\theta_1) = n_2 \sin(\theta_2)$
$n_1 = \text{refractive index of first medium}$
$\theta_1 = \text{angle of incidence}$
$n_2 = \text{refractive index of second medium}$
$\theta_2 = \text{angle of refraction}$
First Medium 第一媒介
n1 = 1
n 1 = 1
Second Medium 第二媒介
n2 = 1.5
Normal 法线
0.013
n2 n 2 1.5
In the above interactive diagram, you can see that:
在上面的交互式示意图中,您可以看到:
- When $n_2 = n_1$, the light ray passes straight through without bending.
当 $n_2 = n_1$ 时,光线会笔直穿过,不发生弯曲。 - When $n_2 > n_1$, the ray bends toward the normal (the imaginary line perpendicular to the surface).
当 $n_2 > n_1$ 时,光线会向法线(垂直于表面的假想线)弯曲。 - When $n_2 < n_1$, 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.
当 $n_2 < n_1$ 时,光线会偏离法线;并且根据入射角不同,它可能弯折得如此厉害,以至于不会穿过界面,而是反射回原来的介质中。
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 $index = 1$ (air).
环境介质的折射率设为 $index = 1$(空气)。 - Use materials with $index > 1$, and prefer $1.5$ (glass).
使用 $index > 1$ 的材料,并优先选择 $1.5$ (玻璃)。 - 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 perspective).
物体是与背景平行的二维形状(无透视)。 - No gap between objects and background plane (only one refraction).
物体与背景平面之间没有间隙(只有一次折射)。 - Circle shapes only in this article:
本文中仅出现圆形图形: Extending to other shapes requires preliminary calculations.
将方法推广到其他形状需要进行初步计算。 Circles let us form rounded rectangles by stretching the middle.
圆形可以通过拉伸中间部分来形成圆角矩形。
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.
我们的玻璃表面由一个数学函数描述,该函数定义了从玻璃边缘到边框末端任意一点的厚度。这个 表面函数 取值范围为 $0$ (外边缘)到 $1$ (边框末端,平面起始处),并返回该点的玻璃高度。
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:
根据高度,我们可以计算入射角,即入射光线与该点表面法线之间的夹角。法线就是该点高度函数的导数,旋转 $-90$ 度:
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:
本文将使用四种不同的高度函数来演示表面形状对折射的影响:
Convex Circle 凸圆
$y = \sqrt{1 - (1 - x) ^ 2}$
Simple circular arc → a spherical dome. Easier than the squircle, but the transition to the flat interior is harsher, producing sharper refraction edges—more noticeable when the shape is stretched away from a true circle.
简单的圆弧对应球形穹顶。它比 squircle 更容易处理,但向内部平面过渡时更生硬,会产生更锐利的折射边缘;当形状被拉伸得不再接近真圆时,这种边缘会更加明显。
Convex Squircle 凸圆角
$y = \sqrt[4]{1 - (1 - x) ^ 4}$
Uses the Squircle Apple favors: a softer flat→curve transition that keeps refraction gradients smooth even when stretched into rectangles—no harsh interior edges. It also makes the bezel appear optically thinner than its physical size because the flatter outer zones bend light less.
它采用了苹果偏爱的圆角矩形设计:一种更柔和的平面到曲面过渡,即使拉伸成矩形也能保持折射梯度平滑,避免生硬的内部边缘。此外,由于较平坦的外圈对光线的折射较小,因此边框在视觉上看起来比实际尺寸更薄。
Concave 凹
$y = 1 - \text{Convex}(x)$
The concave surface is the complement of the convex function, creating a bowl-like depression. This surface causes light rays to diverge outward, displacing them beyond the glass boundaries.
凹面是凸函数的补面,会形成一种碗状凹陷。这种表面会让光线向外发散,把采样位置推出玻璃边界之外。
Lip 唇缘
$y = \text{mix}(\text{Convex}(x), \text{Concave}(x), \text{Smootherstep}(x))$
Blends convex and concave via Smootherstep: raised rim, shallow center dip.
通过 Smootherstep 混合凸面与凹面,得到凸起边缘和浅凹中心的组合轮廓。
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, we 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 for the Switch component, covered later).
我们希望避免外部位移,因为它需要对物体外部的背景进行采样。苹果的 Liquid Glass 似乎更倾向于凸面轮廓(Switch 组件除外,稍后会介绍)。
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.
这样,我们就可以一次性计算物体一个“半切片”上的所有二维数据(x 轴和 z 轴),然后绕 z 轴旋转这些预先计算好的位移。
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.)
我们实际需要对一个半径进行 127 次光线模拟的采样次数,是由 SVG 位移贴图的分辨率限制决定的。(参见下一节。)
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.
要在位移贴图中使用这些向量,我们需要对它们进行归一化。归一化是指将向量缩放,使其最大值为 $1$ ,这样就可以将它们表示在一个固定的范围内。
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: magnitude / maximumDisplacement,
};
We store maximumDisplacement as we will need it to re-scale the displacement map back to the actual magnitudes.
我们存储 maximumDisplacement ,因为我们需要它来将位移图重新缩放回实际大小。
SVG Displacement Map SVG 位移贴图
Now we need to translate our mathematical refraction calculations into something the browser can actually render. We’ll use SVG displacement maps.
现在我们需要将数学上的折射计算结果转换成浏览器可以实际渲染的内容。我们将使用 SVG 置换贴图 。
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.
SVG 的 <feDisplacementMap /> 将这些像素编码为 32 位 RGBA 图像,其中每个通道代表不同的位移轴。
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.
用户可自行定义哪个通道对应哪个轴,但理解其限制至关重要:由于每个通道为 8 位,因此位移量在每个方向上都被限制在 -128 到 127 像素的范围内(总共有 256 个可能的值)。128 为中性值,表示无位移。
SVG filters can only use images as displacement maps, so we need to convert our displacement vector field into an image format.
SVG 滤镜只能使用图像作为位移贴图,因此我们需要将位移矢量场转换为图像格式。
<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.
<feDisplacementMap /> 使用红色通道作为 X 轴,绿色通道作为 Y 轴。蓝色通道和 alpha 通道将被忽略。
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):
红色 (X) 和绿色 (Y) 通道均为 8 位值 (0–255)。在不进行任何额外缩放的情况下,它们线性映射到 [−1, 1] 范围内的归一化位移,其中 128 为中性值(无位移):
$$ \begin{aligned} 0 &\mapsto -1 \ 128 &\mapsto 0 \ 255 &\mapsto 1 \end{aligned} $$
The scale attribute of <feDisplacementMap /> multiplies this normalized amount:
<feDisplacementMap /> 的 scale 属性会将此归一化值乘以:
$$ \begin{aligned} 0 &\mapsto -scale \ 128 &\mapsto 0 \ 255 &\mapsto scale \end{aligned} $$
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:
由于我们的向量以最大可能的位移(以像素为单位)为单位进行归一化,因此我们可以直接将该最大值用作滤波器的 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).
您还可以通过 scale 来淡入/淡出效果——无需重新计算贴图(即使物理上不精确,也对艺术控制很有用)。
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.
为了将位移矢量场转换为位移贴图,我们需要将每个矢量转换为颜色值。红色通道代表矢量的 X 分量,绿色通道代表 Y 分量。
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.
我们目前每个向量都有极坐标(角度和大小),因此我们需要将它们转换为笛卡尔坐标(X 和 Y),然后再映射到红色和绿色通道。
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.
因为我们已经对向量进行了归一化,所以这里的 magnitude 介于 0 和 1 之间。
From here, we just remap the values to the range of 0 to 255 for the red and green channels:
接下来,我们只需将红色和绿色通道的值重新映射到 0 到 255 的范围内:
Red: 255
X axis: 1.00
Green: 127
Y axis: -0.00
Result (Blended) 结果(混合)
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.
将地图中的每个矢量转换为颜色值后,我们就得到了一张可以用作 SVG 滤镜中的位移贴图的图像。
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.
这个演示环境将 SVG 置换滤镜应用于一个简单的场景,并允许您调整表面形状、边框宽度、玻璃厚度和效果比例。观察这些参数如何改变折射场、生成的置换贴图以及最终渲染效果。
Surface 表面
Controls 控制
Radius Simulation 半径模拟
Displacement Map 位移图
Radius 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.
我们液态玻璃效果的最后一部分是 镜面高光 ——当光线以特定角度照射到真正的玻璃物体上时,你会看到那些明亮、闪亮的边缘。
The way Apple implements it seems to be a simple rim light effect, where the highlight appears around the edges of the glass object, and its intensity varies based on the angle of the surface normal relative to a fixed light direction.
苹果的实现方式似乎是简单的边缘光效果,高光出现在玻璃物体的边缘,其强度根据表面法线相对于固定光线方向的角度而变化。
-180°0°180°
Combining Refraction and Specular Highlight 结合折射和镜面高光
In the final SVG filter, we combine both the displacement map for refraction and the specular highlight effect.
在最终的 SVG 滤镜中,我们将折射的位移贴图和镜面高光效果结合起来。
Both are loaded as separate <feImage /> elements, and then combined using <feBlend /> to overlay the highlight on top of the refracted image.
两者分别作为单独的 <feImage /> 元素加载,然后使用 <feBlend /> 将它们组合起来,以将高光叠加到折射图像之上。
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 SVG 滤镜作为 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:
至此,跨浏览器兼容性就结束了。目前只有 Chrome 支持使用 SVG 滤镜作为 backdrop-filter ,这对于将液态玻璃效果应用于 UI 组件至关重要:
.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.
现在我们已经具备了所有条件,可以创建利用这种效果的组件了。
Conclusion 结论
This prototype distills Apple’s Liquid Glass into real‑time refraction plus a simple highlight. It’s flexible, but still Chrome‑bound—only Chromium exposes SVG filters as backdrop-filter. That said, it’s already viable inside Chromium‑based runtimes like Electron, elsewhere you could fake a softer fallback with layered blur.
这个原型将苹果的 Liquid Glass 技术简化为实时折射加上简单的高光效果。它很灵活,但仍然受限于 Chrome 浏览器——只有 Chromium 才能将 SVG 滤镜作为 backdrop-filter 公开。也就是说,它在基于 Chromium 的运行时环境(例如 Electron)中已经可以正常运行,在其他环境中,你可以使用分层模糊来模拟更柔和的回退效果。
Treat this strictly as experimental. Dynamic shape/size changes are currently costly because nearly every tweak (besides animating <filter /> props, like scale) forces a full displacement map rebuild.
请严格将其视为实验性功能。动态形状或大小的更改目前开销很大,因为几乎每次调整(除了给 <filter /> 的属性如 scale 做动画)都会强制重建完整的位移贴图。
The code needs a cleanup pass and perf work before any possible open‑source release.
在发布任何可能的开源版本之前,代码需要进行清理和性能优化。
Thanks for reading my first post—I’d genuinely love any feedback, ideas, critiques, or suggestions. If it sparked a thought or you know someone who’d enjoy this kind of deep‑dive, feel free to pass it along.
感谢阅读我的第一篇文章——我真心希望得到任何反馈、想法、批评或建议。如果它引发了你的思考,或者你认识有人会喜欢这种深入探讨的内容,请随时分享给他们。