如果说布局决定了页面的“骨架”,那么渐变与阴影决定的,就是页面的“皮肤与光线”。它们是 CSS 里最容易被低估、也最容易被用滥的两组能力:linear-gradient 谁都会写,可真正做出有质感、有层次、还能保持 60fps 的视觉效果,需要的远不止“两个颜色加个角度”。渐变负责光,阴影负责空间。理解了这一点,你才能明白为什么有些页面一眼看上去就“高级”,而另一些无论怎么堆颜色都显得廉价。本篇 35 分钟长文,从渐变语法与颜色空间讲起,穿过渐变文字、渐变边框、遮罩渐隐、conic-gradient 的几何魔法、@property 驱动的动态渐变,再深入到 box-shadow 的三层结构、多层阴影、彩色阴影、玻璃拟态与暗色模式主题化,并配以多个可实时拖拽的实验台,帮您把这两项技能真正练成肌肉记忆。
为什么渐变与阴影值得单独拿出来讲
在现代 Web 设计体系里,颜色与光影承担了远超“好看”的职责。它们是信息层级的一部分:主按钮用高饱和渐变吸引点击,卡片用柔和阴影从背景中“浮起”,禁用状态用降饱和的灰调降低存在感。用户不会告诉你“这个阴影做得好”,但他们会本能地觉得这个界面“更可信、更精致”。
更现实的原因是性能。渐变与阴影都属于绘制(Paint)层的属性。它们不像 width、left 那样会触发布局重排,但如果面积过大、动画方式不当,同样能让主线程和 GPU 一起崩溃。一个覆盖全屏、每帧重新计算的动态渐变,比一个简单的位移动画要昂贵得多。
/* 1. 用极少的代码,创造远超纯色的视觉丰富度 */
.hero {
background: linear-gradient(135deg, #1e3a8a, #2563eb);
}
/* 2. 替代图片,减少请求与体积 */
/* 一张 1920×600 的 PNG 渐变背景可能 200KB+ */
/* 同样的 CSS 渐变只有几十字节,且任意分辨率不糊 */
/* 3. 阴影建立空间层级,让界面“有厚度” */
.card {
box-shadow: 0 1px 2px rgba(15,23,42,0.06),
0 8px 24px rgba(15,23,42,0.08);
}
先建立一个判断标准
在动手之前,先问自己三个问题,它们几乎能解决 80% 的“为什么我做出来不高级”:
- 光源在哪?页面中所有阴影的方向应当一致,通常默认“光从上方偏左打下来”,即阴影向下偏右。
- 渐变想要表达什么?是模拟光照(用径向渐变),还是表达方向(用线性渐变),还是表达占比(用锥形渐变)?
- 它会不会动?静态渐变可以尽情复杂,动态渐变必须极其克制。
渐变语法全景:三种渐变,三种思维方式
CSS 渐变本质上是一张由浏览器实时生成的图片。它出现在所有能写 <image> 的地方:background、border-image、mask-image、list-style-image,甚至 content。理解这一点,很多“奇怪”的用法就顺理成章了。
三种渐变类型并不是同一种东西的三种写法,它们各自回答不同的问题:线性渐变回答“往哪个方向”,径向渐变回答“从哪个中心扩散”,锥形渐变回答“绕着中心怎么转”。
background: linear-gradient(135deg, #2563eb, #ec4899);
/* 角度:0deg 向上,90deg 向右,顺时针增加 */
/* 也可以用 to right / to bottom left 等关键词 */
/* ② 径向渐变:表达光源、聚焦、晕染 */
background: radial-gradient(circle at 30% 30%, #8b5cf6, #0f172a);
/* 形状:circle / ellipse */
/* 尺寸:closest-side / farthest-corner / 200px */
/* ③ 锥形渐变:表达角度、占比、旋转 */
background: conic-gradient(from 0deg, #2563eb, #8b5cf6, #ec4899, #2563eb);
/* 角度单位:deg / turn / rad */
/* 1turn = 360deg,用 turn 写占比更直观 */
/* ④ 重复渐变:图案生成器 */
background: repeating-linear-gradient(45deg, #eef2f7 0 10px, #ffffff 10px 20px);
background: repeating-conic-gradient(#e2e8f0 0 25%, #ffffff 0 50%);
/* ⑤ 多背景叠加:真正的“高级感”来源 */
background:
radial-gradient(circle at 20% 20%, rgba(37,99,235,0.35), transparent 45%),
radial-gradient(circle at 80% 60%, rgba(236,72,153,0.32), transparent 50%),
linear-gradient(135deg, #0f172a, #1e293b);
一个高频误区:渐变其实是“图片”,不是“颜色”
因为它属于 <image> 类型,所以不能直接用在 background-color 上,也不能参与 transition 的平滑插值(至少在 @property 出现之前是这样)。同样,color、border-color、fill 这些只接受 <color> 的属性,也写不了渐变。记住这句话,能省下大量调试时间。
.bad { background-color: linear-gradient(red, blue); }
/* ✅ 正确:用 background 或 background-image */
.good { background: linear-gradient(red, blue); }
/* 或者保留底色作为降级: */
.good2 {
background-color: #2563eb;
background-image: linear-gradient(red, blue);
}
色标与插值:决定渐变“顺不顺”的关键
很多人写渐变只给两个颜色,剩下的全交给浏览器。但真正让渐变显得高级的,往往是那些被精心安排过的色标位置与硬边过渡。
色标位置:控制过渡发生的区间
每个颜色后面可以跟一个或多个位置值(百分比或长度)。不写位置时,浏览器会自动均匀分布;一旦写了,过渡就会被“压缩”到指定区间里。
background: linear-gradient(90deg, #2563eb, #8b5cf6, #ec4899);
/* 三个色标平均分布在 0% / 50% / 100% */
/* 手动指定位置:让前段快速过渡,后段缓慢 */
background: linear-gradient(90deg,
#2563eb 0%,
#8b5cf6 20%,
#ec4899 85%,
#f59e0b 100%);
/* 硬边过渡:两个色标写在同一位置 */
background: linear-gradient(90deg,
#2563eb 0 50%,
#ec4899 50% 100%);
/* 结果是一条干净利落的分界线,没有模糊地带 */
/* 用硬边 + repeating 生成条纹 */
background: repeating-linear-gradient(
45deg,
#eef2f7 0 12px,
#ffffff 12px 24px
);
透明度的陷阱:别用 transparent
这是渐变里最经典的坑之一。transparent 在绝大多数浏览器里等价于 rgba(0, 0, 0, 0),也就是透明黑。当它和鲜艳颜色做渐变时,中间会出现一段明显的灰黑色过渡带,颜色看起来“脏”。
linear-gradient(to bottom, #2563eb, transparent)中间会经过“透明黑”,出现灰暗带。
linear-gradient(to bottom, #2563eb, rgba(37,99,235,0))保持同一个色相,只是降低 alpha,过渡干净。
.fade-1 { background: linear-gradient(to bottom, #2563eb, rgb(37 99 235 / 0)); }
/* 用 color-mix() 让颜色与透明色混合 */
.fade-2 {
background: linear-gradient(to bottom, #2563eb, color-mix(in srgb, #2563eb 0%, transparent));
}
/* 多个遮罩层同时淡出,效果更自然 */
.fade-3 {
background:
linear-gradient(to bottom, rgb(37 99 235 / 0.9), rgb(37 99 235 / 0)),
radial-gradient(circle at 50% 0%, rgb(139 92 246 / 0.6), transparent 60%),
#0f172a;
}
颜色空间的进化:从 sRGB 到 oklch
这是近几年 CSS 颜色领域最重要的变化之一。传统渐变默认在 sRGB 空间里做线性插值,这意味着从蓝到黄,中间会经过一段发灰、发暗的过渡——因为 sRGB 的数值插值并不符合人眼对亮度的感知。
而 in oklch 让插值发生在感知均匀的色彩空间里,中间色的明度与饱和度变化更符合直觉,过渡明显更“亮”、更“干净”。
background: linear-gradient(in oklch, #2563eb, #facc15);
background: linear-gradient(in oklab, #2563eb, #facc15);
background: linear-gradient(in hsl, #2563eb, #facc15);
background: linear-gradient(in srgb-linear, #2563eb, #facc15);
/* 直接用 oklch() 定义颜色:L 明度 / C 彩度 / H 色相 */
.brand {
--brand: oklch(0.62 0.21 258); /* 蓝 */
--brand-light: oklch(0.82 0.12 258);
--brand-dark: oklch(0.42 0.18 258);
}
/* 只改明度,就能得到一整套和谐的色阶 */
background: linear-gradient(135deg, oklch(0.72 0.16 258), oklch(0.42 0.18 258));
/* 色相插值:长路径 vs 短路径 */
background: linear-gradient(in oklch longer hue, #2563eb, #facc15);
background: linear-gradient(in oklch shorter hue, #2563eb, #facc15);
为什么 oklch 对渐变如此重要
在 sRGB 中,#2563eb(蓝)和 #facc15(黄)的中点是一个浑浊的橄榄灰。而在 oklch 中,中点仍然保持着可观的彩度与明度。这不是审美问题,而是数值模型与实际感知之间的偏差。当你需要做品牌渐变色阶、按钮悬停状态、暗色模式配色时,oklch 会显著降低“调色调到手酸”的概率。
实践建议:用 oklch 做色阶生成,用 srgb 做兜底降级。由于浏览器对 oklch() 的支持已经相当广泛(Chrome 111+、Safari 15.4+、Firefox 113+),生产环境可以放心使用,但记得用 @supports 包一层。
.btn {
background: linear-gradient(135deg, #3b82f6, #1d4ed8);
}
@supports (color: oklch(0.6 0.2 250)) {
.btn {
background: linear-gradient(135deg, oklch(0.68 0.16 254), oklch(0.48 0.19 262));
}
}
交互实验室:渐变生成器
下面这个实验台把渐变的三个核心参数全部暴露出来:类型、角度、颜色。拖动滑块、切换类型,上方预览会实时变化,下方同步生成可直接复制的 CSS 代码。
试着观察这几件事:
- 把角度从 0 拖到 360,注意
linear-gradient的方向是顺时针旋转的; - 切换到
radial-gradient后,角度参数会变成光心的偏移位置; - 切换到
conic-gradient后,颜色会绕着中心环绕一周,首尾相接; - 三个颜色之间如果色相跨度太大,中间会显得脏——这正是上一节 oklch 要解决的问题。
高级技巧一:渐变文字与描边文字
渐变文字的实现核心只有两行:background-clip: text 把背景裁剪到文字形状,再把 color 设为透明,让背景透出来。看似简单,但里面有几个必须注意的细节。
.gradient-text {
background: linear-gradient(90deg, #2563eb, #ec4899);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
/* 让渐变流动起来 */
.flow-text {
background: linear-gradient(90deg, #2563eb, #8b5cf6, #ec4899, #f59e0b, #2563eb);
background-size: 300% 100%;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
animation: textFlow 6s linear infinite;
}
@keyframes textFlow {
from { background-position: 0% 50%; }
to { background-position: 300% 50%; }
}
/* 描边文字(空心字) */
.stroke-text {
color: transparent;
-webkit-text-stroke: 2px #2563eb;
}
/* 描边 + 渐变填充:两层叠加 */
.stroke-grad {
position: relative;
color: transparent;
-webkit-text-stroke: 1px rgba(37,99,235,0.5);
background: linear-gradient(90deg, #2563eb, #ec4899);
-webkit-background-clip: text;
background-clip: text;
}
渐变文字 + 阴影的正确姿势
text-shadow 绘制在文字字形之上,而背景裁剪出的渐变也在字形范围内。两者叠加时,阴影会遮挡渐变。解决办法是用伪元素复制文字:一层负责渐变填充,一层负责阴影。
.fancy-title {
position: relative;
background: linear-gradient(90deg, #2563eb, #ec4899);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.fancy-title::after {
content: attr(data-text);
position: absolute;
inset: 0;
z-index: -1;
color: transparent;
text-shadow: 0 6px 18px rgba(37, 99, 235, 0.35);
}
注意 z-index: -1 需要父元素有 position: relative 且不产生新的层叠上下文,否则伪元素可能被压到父元素背景之下。如果遇到问题,可以改用两层真实元素。
高级技巧二:渐变边框的四种实现
CSS 没有 border-gradient 这个属性,所以渐变边框一直是个“需要技巧”的需求。市面上流传着至少四种方案,各有取舍。
.border-a {
border: 3px solid transparent;
border-radius: 12px;
background:
linear-gradient(#fff, #fff) padding-box,
linear-gradient(135deg, #2563eb, #ec4899) border-box;
}
/* ② mask 复合(边框可完全透明,适合叠加) */
.border-b {
padding: 3px;
border-radius: 12px;
background: linear-gradient(135deg, #8b5cf6, #10b981);
-webkit-mask:
linear-gradient(#000 0 0) content-box,
linear-gradient(#000 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
}
/* ③ border-image(简单,但圆角会失效) */
.border-c {
border: 3px solid transparent;
border-image: linear-gradient(135deg, #f59e0b, #ef4444) 1;
}
/* ④ 伪元素(可控性最强) */
.border-d {
position: relative;
border-radius: 12px;
}
.border-d::before {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
padding: 2px;
background: linear-gradient(135deg, #2563eb, #8b5cf6);
pointer-events: none;
}
| 方案 | 圆角 | 推荐度 |
|---|---|---|
| 双 background | ✅ 完美 | 首选 |
| mask 复合 | ✅ 完美 | 推荐 |
| border-image | ❌ 失效 | 谨慎 |
| 伪元素 | ✅ 完美 | 较繁琐 |
让边框转起来:conic-gradient + @property
双 background 方案有一个天然优势:它可以直接把 border-box 那一层换成 conic-gradient,再用 @property 声明一个可动画的角度变量,边框就会自己旋转起来。
边框就会持续旋转,而完全不触发重排。
@property --spin {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
.aurora-box {
position: relative;
padding: 3px;
border-radius: 16px;
background: conic-gradient(from var(--spin),
#2563eb, #8b5cf6, #ec4899, #f59e0b, #10b981, #2563eb);
animation: spinBorder 4s linear infinite;
}
@keyframes spinBorder {
to { --spin: 360deg; }
}
/* 内层用深色背景盖住中间,只露出边框 */
.aurora-box .inner {
background: #0f172a;
border-radius: 14px;
padding: 22px 20px;
}
/* 降级:不支持 @property 时,用静态渐变边框 */
@supports not (background: conic-gradient(from var(--spin), red, blue)) {
.aurora-box { animation: none; }
}
这个技巧同样适用于加载环、进度指示器、扫描线效果。关键是 @property 让自定义属性有了类型,浏览器才知道如何在 0deg 和 360deg 之间做插值。
高级技巧三:渐变遮罩与内容渐隐
渐变不仅能当背景,还能当遮罩。mask-image 接受任何 <image>,包括渐变。这意味着你可以用一条黑白渐变,控制元素哪些部分可见、哪些部分透明。
.scroll-fade {
overflow-y: auto;
mask-image: linear-gradient(
to bottom,
transparent 0,
#000 28px,
#000 calc(100% - 28px),
transparent 100%
);
}
/* 图片底部渐隐融入背景 */
.img-fade {
mask-image: linear-gradient(to bottom, #000 55%, transparent 100%);
}
/* 径向遮罩:聚焦中心 */
.spotlight {
mask-image: radial-gradient(circle at 50% 50%, #000 30%, transparent 72%);
}
/* 用 mask 做内阴影 / 内发光 */
.inner-glow {
position: relative;
}
.inner-glow::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: inset 0 0 24px rgba(37,99,235,0.45);
pointer-events: none;
}
第一条内容:向上滚动,顶部会逐渐淡出。
第二条内容:这就是 mask-image 的功劳。
第三条内容:无需任何 JavaScript。
第四条内容:纯 CSS 实现内容渐隐。
第五条内容:底部同样会平滑淡出。
第六条内容:可以放心用在长列表上。
试着滚动上面的容器
mask 与 -webkit-mask 的兼容写法
虽然现代浏览器已经支持无前缀的 mask-image,但在一些较旧的 WebKit 内核里,仍需要 -webkit-mask-image。稳妥的写法是两个都写,且把标准属性放在后面。
-webkit-mask-image: linear-gradient(to bottom, #000, transparent);
mask-image: linear-gradient(to bottom, #000, transparent);
-webkit-mask-repeat: no-repeat;
mask-repeat: no-repeat;
-webkit-mask-size: 100% 100%;
mask-size: 100% 100%;
}
conic-gradient 的几何魔法
锥形渐变是三种渐变里最晚被广泛支持的,却可能是最有想象力的一种。它以中心为原点,按角度方向扫过一圈,非常适合表达占比、进度、旋转、环绕。
.pie {
width: 120px;
height: 120px;
border-radius: 50%;
background: conic-gradient(
#2563eb 0turn 0.35turn,
#8b5cf6 0.35turn 0.6turn,
#f59e0b 0.6turn 0.8turn,
#e2e8f0 0.8turn 1turn
);
}
/* ② 环形进度:用伪元素挖空中心 */
.donut {
position: relative;
border-radius: 50%;
background: conic-gradient(#10b981 0turn 0.45turn, #e2e8f0 0.45turn 1turn);
}
.donut::after {
content: '45%';
position: absolute;
inset: 22px;
border-radius: 50%;
background: white;
display: flex;
align-items: center;
justify-content: center;
}
/* ③ 色相环 */
.wheel {
border-radius: 50%;
background: conic-gradient(
#ef4444, #f59e0b, #eab308, #22c55e,
#06b6d4, #2563eb, #8b5cf6, #ec4899, #ef4444
);
}
/* ④ 棋盘格:conic-gradient + background-size */
.checker {
background-image: conic-gradient(
#e2e8f0 0.25turn, #ffffff 0.25turn 0.5turn,
#e2e8f0 0.5turn 0.75turn, #ffffff 0.75turn
);
background-size: 30px 30px;
}
/* ⑤ 圆锥光扫过效果 */
.scan {
background: conic-gradient(
from 0deg at 50% 50%,
rgba(37,99,235,0.6) 0deg,
transparent 60deg,
transparent 360deg
);
}
拖动下面的滑块,实时改变饼图的第一段占比:
@property:让渐变第一次真正“可动画”
在 @property 出现之前,CSS 渐变几乎是无法平滑动画的。因为浏览器不知道两个渐变之间该如何插值,只能做“硬切换”。@property 的出现改变了这一切:它允许你声明一个自定义属性的类型,从此浏览器就知道该怎么在数值之间做过渡。
.bad {
background: linear-gradient(#2563eb, #ec4899);
transition: background 0.5s; /* 会跳变 */
}
/* ✅ 方案一:动画 background-position */
.bg-shift {
background: linear-gradient(90deg, #2563eb, #8b5cf6, #ec4899, #2563eb);
background-size: 300% 100%;
animation: bgShift 8s linear infinite;
}
@keyframes bgShift {
from { background-position: 0% 50%; }
to { background-position: 300% 50%; }
}
/* ✅ 方案二:注册角度变量,让渐变方向旋转 */
@property --grad-angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
.angle-spin {
background: linear-gradient(var(--grad-angle), #2563eb, #ec4899);
animation: angleSpin 5s linear infinite;
}
@keyframes angleSpin {
to { --grad-angle: 360deg; }
}
/* ✅ 方案三:注册颜色变量,做真正的颜色过渡 */
@property --c1 {
syntax: '<color>';
initial-value: #2563eb;
inherits: false;
}
@property --c2 {
syntax: '<color>';
initial-value: #ec4899;
inherits: false;
}
.color-shift {
background: linear-gradient(135deg, var(--c1), var(--c2));
transition: --c1 0.6s ease, --c2 0.6s ease;
}
.color-shift:hover {
--c1: #f59e0b;
--c2: #10b981;
}
三种方案怎么选
- background-position:兼容性最好,性能也最可控,适合大面积背景的“流动”效果。
- @property 角度:适合边框、光扫、雷达这类需要旋转的场景,视觉冲击力强。
- @property 颜色:适合悬停变色、主题切换,代码最干净,但每帧重绘面积大时要谨慎。
无论用哪种,都要记住一条铁律:动态渐变永远比静态渐变贵得多。如果它覆盖了半个屏幕,那就值得重新考虑设计。
阴影的三层结构:box-shadow / text-shadow / drop-shadow
CSS 里有三种阴影,它们看起来相似,行为却完全不同。很多人分不清,导致选错工具、做无用功。
| 属性 | 作用对象 | 跟随形状 | 适用场景 |
|---|---|---|---|
box-shadow |
元素盒子 | 否(永远矩形 / 圆角矩形) | 卡片、按钮、面板 |
text-shadow |
文字字形 | 是(跟随每个字符轮廓) | 标题发光、文字可读性增强 |
filter: drop-shadow() |
渲染后的像素 | 是(跟随透明通道) | PNG / SVG 图标、不规则图形 |
box-shadow:
inset? /* 可选:内阴影 */
offset-x offset-y
blur-radius?
spread-radius?
color?;
/* 实例:一个“刚刚浮起”的卡片 */
.card {
box-shadow: 0 1px 2px rgba(15,23,42,0.06),
0 8px 24px rgba(15,23,42,0.08);
}
/* 内阴影:制造凹陷、输入框、Pressed 状态 */
.pressed {
box-shadow: inset 0 2px 6px rgba(15,23,42,0.18);
}
/* text-shadow:文字发光 */
.glow-text {
text-shadow: 0 0 8px rgba(37,99,235,0.8),
0 0 22px rgba(37,99,235,0.45);
}
/* drop-shadow:跟随不规则形状 */
.png-icon {
filter: drop-shadow(0 6px 10px rgba(15,23,42,0.25));
}
/* 对比:box-shadow 在不规则图形上的“穿帮” */
.triangle {
width: 0;
height: 0;
border-left: 40px solid transparent;
border-right: 40px solid transparent;
border-bottom: 60px solid #2563eb;
/* box-shadow 会沿着元素矩形绘制,而不是三角形 */
}
blur-radius 与 spread-radius 的区别
这是最容易被混淆的两个参数。简单来说:blur 让阴影边缘变模糊,spread 让阴影整体变大或变小。spread 为正值时,阴影会向外扩张,即使元素本身很小,也能做出“大面积弥散光”的效果;spread 为负值时,阴影会收缩,常用于做描边效果。
box-shadow: 0 10px 10px rgba(15,23,42,0.2);
box-shadow: 0 10px 40px rgba(15,23,42,0.2);
/* spread 为负:收缩成一条细边 */
box-shadow: 0 0 0 2px rgba(37,99,235,0.6); /* 描边 */
box-shadow: 0 0 0 -6px #2563eb; /* 几乎不可见 */
/* spread 配合 blur,做出弥散光晕 */
box-shadow: 0 20px 60px 10px rgba(139, 92, 246, 0.35);
交互实验室:阴影生成器
阴影的手感来自五个参数的配合。下面这个实验台把它们全部开放,拖动任意滑块,观察阴影如何一步步从“生硬”变成“自然”。
调参经验:
- 垂直偏移应大于水平偏移,因为默认光源在上方。
- 模糊半径约为偏移量的 2 倍时,阴影看起来最自然。
- 不要用纯黑:
rgba(15,23,42,·)这类带蓝调的深色比#000更通透。 - 不透明度控制在 0.05~0.25 之间,超过就会显得脏。
多层阴影:高级质感的真正来源
单层阴影永远做不出真实的光影。现实中的阴影有接触阴影(紧贴物体的深色暗部)和环境阴影(大面积、柔和、颜色更淡的散射),两者叠加才有立体感。
:root {
--elev-1:
0 1px 2px rgba(15,23,42,0.06),
0 1px 1px rgba(15,23,42,0.04);
--elev-2:
0 2px 4px rgba(15,23,42,0.06),
0 8px 20px rgba(15,23,42,0.08);
--elev-3:
0 4px 8px rgba(15,23,42,0.06),
0 12px 28px rgba(15,23,42,0.10),
0 32px 64px rgba(15,23,42,0.12);
}
/* 悬停时提升层级:只动阴影,不动布局 */
.card {
box-shadow: var(--elev-1);
transform: translateY(0);
transition: box-shadow 0.25s ease, transform 0.25s ease;
}
.card:hover {
box-shadow: var(--elev-3);
transform: translateY(-4px);
}
/* 新拟态(Neumorphism):双向阴影 */
.neu {
background: #eef2f7;
border-radius: 16px;
box-shadow:
8px 8px 16px rgba(163,177,198,0.6),
-8px -8px 16px rgba(255,255,255,0.9);
}
/* 右下是暗部,左上是高光 —— 模拟凸起 */
多层阴影的性能提醒
每多一层阴影,浏览器就多一次绘制。三层以内的阴影对性能影响很小;但如果一个页面上有几百个卡片,每个都带四层阴影,在低端设备上依然会感受到滚动卡顿。
如果确实需要大量卡片同时展示,可以考虑:
- 用
contain: paint限制重绘范围; - 用
content-visibility: auto跳过屏幕外卡片的渲染; - 把阴影做成静态图片或用
filter: blur()的伪元素替代——但要注意伪元素方案同样有代价。
彩色阴影与长阴影:让光有颜色
黑色阴影是最安全的,也是最平庸的。当阴影颜色与元素本身的色相一致时,画面会立刻变得“通透”起来——这几乎是所有高级 UI 的共同秘密。
.btn-bad {
background: #2563eb;
box-shadow: 0 10px 20px rgba(0,0,0,0.35);
}
/* ✅ 高级:同色系阴影 */
.btn-good {
background: #2563eb;
box-shadow: 0 10px 24px rgba(37, 99, 235, 0.42);
}
/* 渐变按钮 + 渐变色的阴影(双色光晕) */
.btn-aurora {
background: linear-gradient(135deg, #2563eb, #8b5cf6);
box-shadow:
0 8px 20px rgba(37,99,235,0.35),
0 16px 40px rgba(139,92,246,0.28);
}
/* 用 color-mix 自动从背景色推导阴影色 */
.btn-auto {
--brand: #2563eb;
background: var(--brand);
box-shadow: 0 10px 24px color-mix(in oklch, var(--brand) 55%, transparent);
}
/* 长阴影:用多重 box-shadow 堆出斜向投影 */
.long-shadow {
color: #2563eb;
text-shadow:
1px 1px 0 #1d4ed8,
2px 2px 0 #1d4ed8,
3px 3px 0 #1d4ed8,
4px 4px 0 #1d4ed8,
5px 5px 0 #1d4ed8;
}
/* 发光效果:用 0 偏移 + 大模糊 */
.neon {
box-shadow:
0 0 6px rgba(37,99,235,0.7),
0 0 18px rgba(37,99,235,0.5),
0 0 42px rgba(37,99,235,0.3);
}
左:纯黑阴影 中:同色阴影 右:双色渐变光晕
用 color-mix() 自动推导阴影色
color-mix() 是近两年非常实用的新函数。它可以在指定颜色空间里混合两种颜色,让阴影色与品牌色自动保持一致。配合 CSS 变量,只需改一处品牌色,整个页面的阴影都会同步更新。
--brand: #2563eb;
--shadow-color: color-mix(in oklch, var(--brand) 55%, transparent);
}
.btn {
background: var(--brand);
box-shadow: 0 10px 24px var(--shadow-color);
}
.btn.secondary {
--brand: #8b5cf6; /* 阴影自动跟着变紫 */
}
/* 也可以用相对颜色语法 */
.btn-alt {
box-shadow: 0 10px 24px rgb(from var(--brand) r g b / 0.42);
}
玻璃拟态:backdrop-filter 的正确打开方式
玻璃拟态(Glassmorphism)曾经席卷整个设计圈,它的核心是 backdrop-filter:把元素背后的内容做模糊处理,从而产生磨砂玻璃的质感。它与 filter 的区别必须记牢——filter 模糊的是自己,backdrop-filter 模糊的是身后的背景。
.glass {
background: rgba(255, 255, 255, 0.18);
backdrop-filter: blur(10px) saturate(150%);
-webkit-backdrop-filter: blur(10px) saturate(150%);
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 14px;
box-shadow: 0 8px 32px rgba(15, 23, 42, 0.25);
}
/* saturate 让背后的颜色更鲜活,避免糊成灰 */
/* 也可以用 brightness 提亮背景 */
.glass-2 {
backdrop-filter: blur(14px) saturate(180%) brightness(1.1);
}
/* 降级:不支持 backdrop-filter 时给一个实色底 */
@supports not (backdrop-filter: blur(1px)) {
.glass { background: rgba(15, 23, 42, 0.72); }
}
/* 注意:backdrop-filter 会创建层叠上下文与合成层 */
/* 大量使用会显著增加 GPU 负担,尤其是在滚动时 */
一个常见问题:为什么模糊“没效果”?
最常见的原因是元素背后没有可模糊的内容。如果玻璃卡片直接放在纯色背景上,backdrop-filter 自然看不出任何变化。必须让它的背后有渐变、图片或鲜艳色块,效果才会显现。
第二个原因是父元素设置了 filter、transform、opacity 等属性,创建了新的层叠上下文,导致 backdrop-filter 的作用范围被限制。排查时可以用 DevTools 的 Layers 面板确认。
性能:渐变与阴影的真实代价
静态的渐变与阴影几乎是“免费”的——它们只在首次绘制时计算一次,之后作为纹理缓存复用。真正昂贵的是让它们动起来,或者让它们大面积地反复重绘。
| 用法 | 触发阶段 | 代价 | 建议 |
|---|---|---|---|
静态 background: linear-gradient() |
Paint(仅首次) | 低 | 放心用,任意复杂 |
静态 box-shadow(1~3 层) |
Paint(仅首次) | 低 | 放心用 |
悬停切换 box-shadow |
Paint | 中 | 小面积可用,避免大面积 |
动画 background-position |
Paint | 中 | 限制动画区域大小 |
动画 --color(@property) |
Paint | 高 | 只在小面积元素上使用 |
大面积 backdrop-filter |
Composite + 纹理拷贝 | 高 | 避免固定定位与滚动区域 |
filter: drop-shadow() |
Paint + Composite | 高 | 优先用 box-shadow 替代 |
linear-gradient 的角度每帧旋转。整个区域每帧都要重新绘制,GPU 带宽迅速被吃满,移动端直接掉到 20fps。
transform: rotate(),让它只走合成。
让阴影“动起来”而不掉帧
如果一定要在悬停时改变阴影,有几种优化思路:
- 用伪元素承载阴影:把阴影放在
::after上,动画它的opacity,而opacity只走合成。 - 减少阴影层数:悬停态用两层,而不是四层。
- 用
will-change: box-shadow提前准备:仅在悬停前一刻添加,动画结束后移除。
.card {
position: relative;
background: white;
border-radius: 12px;
box-shadow: 0 1px 2px rgba(15,23,42,0.06);
}
.card::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: 0 18px 40px rgba(15,23,42,0.18);
opacity: 0;
transition: opacity 0.25s ease;
pointer-events: none;
}
.card:hover::after {
opacity: 1;
}
/* 注意:::after 需要在内容之上还是之下? */
/* 若卡片有背景,需给 ::after 设 z-index: -1 并让卡片本身不产生层叠上下文 */
/* 更稳妥的做法是把阴影伪元素放在卡片外层容器上 */
用 DevTools 验证
- 打开 Rendering → Paint flashing,观察动画时哪些区域在闪烁重绘;
- 打开 Rendering → Frame Rendering Stats,实时查看 FPS;
- 在 Performance 面板录制,看 Paint 的耗时占比;
- 在 Layers 面板检查合成层数量与内存占用。
暗色模式下的渐变与阴影
直接给亮色主题的阴影“调暗一点”,在暗色模式下几乎必然失败。原因是:暗色界面里,光的关系是反过来的。亮色主题靠阴影做“下沉”,暗色主题更多地靠高光做“浮起”。
:root {
--surface: #ffffff;
--bg: #f1f5f9;
--text: #0f172a;
--brand-1: oklch(0.60 0.20 258);
--brand-2: oklch(0.62 0.24 320);
--shadow-sm: 0 1px 2px rgba(15,23,42,0.06);
--shadow-lg: 0 18px 40px rgba(15,23,42,0.16);
--highlight: transparent;
}
@media (prefers-color-scheme: dark) {
:root {
--surface: #1e293b;
--bg: #0f172a;
--text: #e2e8f0;
/* 暗色下降低明度与彩度,避免刺眼 */
--brand-1: oklch(0.52 0.16 258);
--brand-2: oklch(0.50 0.18 320);
/* 阴影更黑、更实,但透明度不必更高 */
--shadow-sm: 0 1px 2px rgba(0,0,0,0.5);
--shadow-lg: 0 18px 40px rgba(0,0,0,0.6);
/* 加一层内高光,模拟边缘受光 */
--highlight: inset 0 1px 0 rgba(255,255,255,0.06);
}
}
/* 组件里同时使用阴影与高光 */
.panel {
background: var(--surface);
color: var(--text);
border-radius: 12px;
box-shadow: var(--shadow-sm), var(--highlight);
}
/* 手动主题切换:给根元素加 data-theme */
[data-theme='dark'] {
--surface: #1e293b;
--bg: #0f172a;
--text: #e2e8f0;
}
暗色模式的三条配色原则
- 不要把亮色的渐变直接反转:应该重新选择明度与彩度,而不是简单地把
#fff换成#000。 - 暗色下阴影要更“实”:因为背景本身就很暗,柔和的大范围阴影几乎看不见,需要更强的对比。
- 用内高光补充轮廓:
inset 0 1px 0 rgba(255,255,255,0.06)能让卡片在暗背景中依然有清晰的边界。
兼容性与渐进增强
渐变与阴影涉及的新特性不少,支持程度差异也比较大。下面是关键特性的现状与降级策略。
/* linear / radial-gradient —— 全平台,含 IE10 */
/* conic-gradient —— Chrome 69+ / Safari 12.1+ */
/* box-shadow / text-shadow—— 全平台 */
/* background-clip: text —— 需 -webkit- 前缀,全平台可用 */
/* mask-image —— Chrome 120+ 无前缀,旧版需前缀 */
/* backdrop-filter —— Chrome 76+ / Safari 9+(前缀) */
/* color-mix() —— Chrome 111+ / Safari 16.2+ */
/* oklch() —— Chrome 111+ / Safari 15.4+ */
/* @property —— Chrome 85+ / Safari 16.4+ */
/* border-image —— 全平台,但不支持圆角 */
/* 渐进增强:先给所有人可用版本 */
.badge {
background: #2563eb;
}
@supports (background: conic-gradient(#000, #fff)) {
.badge {
background: conic-gradient(from 180deg, #2563eb, #8b5cf6, #2563eb);
}
}
/* 用 @supports 检测 oklch */
@supports (color: oklch(0.6 0.2 250)) {
.hero {
background: linear-gradient(in oklch, oklch(0.72 0.15 258), oklch(0.42 0.18 262));
}
}
/* JS 侧的特性检测 */
const canConic = CSS.supports('background', 'conic-gradient(red, blue)');
const canBackdrop = CSS.supports('backdrop-filter', 'blur(4px)');
const canProperty = CSS.supports('background', 'conic-gradient(from var(--a), red, blue)');
可以放心作为基础
几乎所有现代浏览器
动画失效但不影响静态显示
需准备实色降级
需要注意的是,@property 属于“渐进增强型”特性:不支持时,动画不会播放,但静态渐变依然正常显示。这正是最理想的降级行为——功能降级,而不是视觉崩溃。
高级渐变与阴影最佳实践清单
把全文要点浓缩成一份可以贴在工位上的清单。
- 用 oklch 定义色阶:明度可控,色阶和谐,中间色不发灰。
- 渐变插值指定颜色空间:
linear-gradient(in oklch, …)几乎总是比默认更好看。 - 不用
transparent做淡出:改用同色相 + alpha 0,避免灰黑过渡带。 - 色标位置是设计的一部分:不要让浏览器自动均匀分布。
- 渐变文字必须同时写
-webkit-前缀,并设置color: transparent。 - 渐变边框首选双 background 方案,圆角与性能都最友好。
- 用 mask-image 做渐隐,比用图片遮罩更轻量。
- conic-gradient 用 turn 单位表达占比,可读性远超百分比。
- 动态渐变优先用 background-position,其次才是
@property。 - 阴影用多层:接触层 + 过渡层 + 环境层,质感立刻不同。
- 阴影颜色跟随元素色相,用
color-mix()自动推导。 - 不要用纯黑阴影:带一点主色调的深色更通透。
- box-shadow 优于 drop-shadow:后者会触发滤镜链,代价明显更高。
- 悬停变色用伪元素 + opacity,而不是直接动画 box-shadow。
- backdrop-filter 只用于小面积,并且必须提供实色降级。
- 暗色模式重新设计光影,而不是简单地把颜色反转。
- 所有新特性都用 @supports 包裹,保证基础体验不崩。
- 用 DevTools 的 Paint flashing 验证,别凭感觉判断性能。
/* 1. 颜色与光影变量 */
:root {
/* 品牌色阶:同一色相,只改明度与彩度 */
--brand-50: oklch(0.96 0.02 258);
--brand-300: oklch(0.78 0.11 258);
--brand-500: oklch(0.62 0.19 258);
--brand-700: oklch(0.46 0.17 262);
/* 渐变配方 */
--grad-brand: linear-gradient(135deg in oklch, var(--brand-300), var(--brand-700));
--grad-aurora: conic-gradient(from 0deg, #2563eb, #8b5cf6, #ec4899, #f59e0b, #2563eb);
/* 阴影层级:由浅入深,三层结构 */
--shadow-1: 0 1px 2px rgba(15,23,42,0.06);
--shadow-2:
0 2px 4px rgba(15,23,42,0.06),
0 8px 20px rgba(15,23,42,0.08);
--shadow-3:
0 1px 2px rgba(15,23,42,0.06),
0 8px 20px rgba(15,23,42,0.09),
0 28px 56px rgba(15,23,42,0.12);
/* 同色系彩色阴影 */
--shadow-brand: 0 10px 26px color-mix(in oklch, var(--brand-500) 45%, transparent);
}
/* 2. 渐变文字工具类 */
.grad-text {
background: var(--grad-brand);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
/* 3. 渐变边框工具类 */
.grad-border {
border: 2px solid transparent;
border-radius: 12px;
background:
linear-gradient(#fff, #fff) padding-box,
var(--grad-brand) border-box;
}
/* 4. 可复用的卡片光影 */
.surface-card {
background: #fff;
border-radius: 14px;
box-shadow: var(--shadow-2);
contain: paint;
transition: box-shadow 0.25s ease, transform 0.25s ease;
}
.surface-card:hover {
box-shadow: var(--shadow-3);
transform: translateY(-3px);
}
/* 5. 无障碍:减少动态效果 */
@media (prefers-reduced-motion: reduce) {
.grad-text,
.aurora-box {
animation: none;
}
.surface-card:hover {
transform: none;
}
}
/* 6. 暗色模式 */
@media (prefers-color-scheme: dark) {
:root {
--brand-500: oklch(0.54 0.16 258);
--shadow-2: 0 2px 6px rgba(0,0,0,0.5), 0 10px 26px rgba(0,0,0,0.45);
--shadow-3: 0 2px 6px rgba(0,0,0,0.55), 0 30px 60px rgba(0,0,0,0.6);
}
.surface-card {
background: #1e293b;
box-shadow: var(--shadow-2), inset 0 1px 0 rgba(255,255,255,0.06);
}
}
渐变与阴影,本质上是用代码模拟光的行为。理解了光源方向、明度关系、色彩一致性这三件事,剩下的就只是语法熟练度而已。真正的分水岭不在于你会不会写 linear-gradient,而在于你是否知道什么时候该用渐变、什么时候该用纯色,什么时候该加阴影、什么时候该让元素干脆贴在背景上。
建议你把这篇文章里的几个实验台当成日常工具:调渐变时打开渐变生成器,调阴影时打开阴影生成器,做暗色模式时对照主题变量那一节。视觉这件事,看一百遍不如亲手拖一次滑块。