☰
CSS背景图叠加渐变:多层背景、混合模式与伪元素实践
2026/10/27 2:36:20 网站建设 项目流程

背景图片上压一层渐变,这类需求我在做落地页、活动banner、卡片封面的时候碰到过太多次。它听起来像个小技巧,实际上牵扯的东西不少:CSS 背景图层的堆叠模型、渐变函数的角度计算、透明色的插值陷阱、混合模式的作用域,还有渲染性能和色带问题。CSS 实现背景图片的渐变,说白了就是搞清楚“渐变在浏览器眼里根本不是颜色,而是一张图”这件事,然后利用多层背景或者伪元素把它和真实图片叠起来。它解决的是三件很具体的事:让图文对比度够、让图片边缘能融进页面、让静态图有动态的视觉氛围。这篇东西写给已经会用 background 简写、但每次叠渐变都要靠试错调参数的前端同学,也写给想把这件事一次做对的设计转前端。下面的参数和代码都是我实际项目里跑过的,不是纸面推演。

1. 先把渐变的身份认清:它是一张图,不是一个颜色

1.1 背景图层的堆叠模型决定了叠加顺序

background-image这个属性接受逗号分隔的多个图层,这是 CSS 背景体系里最容易被低估的一条规则。多个图层按声明顺序从上层往下层堆,写在前面的盖在后面上面,background-color永远垫在最底下。每一层的background-size、background-position、background-repeat、background-origin、background-clip也是逗号分隔、按顺序一一对应的。这个模型是“图片上叠渐变”的全部基础:只要把渐变当作第一个图层、图片当作第二个图层,就得到了一张带渐变的背景图。

很多人卡住是因为脑子里有个错误模型,觉得background只能放一张图,渐变得用额外的div或者::after才能盖上去。真不是。多层背景的原生支持已经很普遍了,代码量比伪元素方案少一半,还能少一层 DOM。代价是每层都要单独写尺寸和位置,漏写一层就会错位,这是后面排查章节要重点讲的地方。

1.2 为什么渐变不需要图片文件却能被当图用

linear-gradient()、radial-gradient()、conic-gradient()这三个函数产出的是浏览器实时生成的光栅图像。既然是图像,就能放在任何接受<image>的位置:background-image、border-image、mask-image、list-style-image,甚至content: url()之外的地方。它和位图最大的区别在尺寸行为:位图默认按原始像素尺寸渲染,渐变默认铺满整个元素的背景定位区域,因为它的固有尺寸不存在,浏览器直接按容器撑开。

这个差别会带来一个实际后果。当你写background-image: linear-gradient(...), url(a.jpg)却不写任何background-size时,渐变会自己铺满,图片则按默认的 auto 尺寸走,两者在宽度大于图片原始宽度的容器里天然不对齐。想让它们严丝合缝地叠在一起,background-size: cover, cover这两项不能省。我见过太多“渐变位置偏了”的问题,根因就是漏了这一句。

1.3 三个渐变函数各自适合什么场景

线性渐变是最常用的工具,linear-gradient(角度或关键词, 色标列表)沿着一条直线插值。角度规则值得记牢:0deg指向正上方,角度顺时针递增,90deg向右,180deg向下。to right等价于90deg,to bottom等价于180deg。这个“向上为零”的习惯来自数学坐标系,跟我们平时看钟表的直觉是反的,第一次用容易写反方向。

径向渐变radial-gradient(形状或尺寸 at 圆心位置, 色标列表)适合做光照、聚焦、暗角。它的形状可以是circle或ellipse,尺寸可以用closest-side、farthest-corner这类关键字控制,也可以用具体长度。做图片四角暗化的时候,radial-gradient(ellipse at center, rgba(0,0,0,0) 40%, rgba(0,0,0,.75) 100%)这一行比叠四个方向的线性渐变省事得多。

锥形渐变conic-gradient从圆心按角度扫一圈,适合做色环、雷达扫描、饼图式遮罩。它和图片搭配的机会少一些,但在“图片以扇形方式揭开”这种转场特效里是唯一顺手的方案。三个函数都支持in oklab之类的插值色彩空间参数,写上之后颜色过渡会均匀得多,尤其在做大跨度色相渐变时,能明显减少中间那段发灰发脏的问题。

2. 三条叠加路线:多层背景、混合模式、伪元素

2.1 多层 background-image:一行搞定,最常用

这是默认选择,代码最简洁,也最好维护。核心思路是把渐变写成第一个图层,图片写在第二个,各层尺寸和位置对齐。渐变的色标用 rgba,让上层的透明度从高到低变化,图片就从下往上透出来。

.hero { min-height: 420px; background-color: #0f172a; background-image: linear-gradient(to right, rgba(10, 20, 40, .88) 0%, rgba(10, 20, 40, .25) 58%, rgba(10, 20, 40, 0) 100%), url("hero.jpg"); background-size: cover, cover; background-position: center, center; background-repeat: no-repeat, no-repeat; }

这段代码里有个细节值得说:background-size的数量必须和background-image的图层数严格一致。数量不匹配时,CSS 不会报错,浏览器会把值列表循环补齐,于是本来想给图片设的尺寸套到了渐变上,视觉上完全错位。这是最难 debug 的一类问题,因为控制台干干净净,没有任何提示。

另一个细节是background-color兜底。图片加载失败时,用户看到的是纯色而不是白板,渐变的对比度也还在。这个习惯我坚持了很多年,尤其是做首屏的时候,图片体积大、加载慢,兜底色能在冷启动那几百毫秒里撑住版面。

2.2 background-blend-mode:让颜色真正参与混合

多层背景是“盖一层半透明膜”,图片本身的色彩并没有被调制,只是被压暗了。如果你想要的是“这张图整体偏冷”“整体偏暖红”这种调色效果,background-blend-mode更合适。它控制的是同一元素内各个背景图层之间怎么混合,可选值包括multiply、screen、overlay、soft-light、color等等。

.tinted { background-color: #1b2a4a; background-image: url("photo.jpg"); background-blend-mode: multiply; background-size: cover; }

这里background-color是参与混合的,multiply会让图片和底色相乘,暗部更暗、整体染上蓝调,非常接近修图软件里的“正片叠底”。

多次叠的时候,background-blend-mode的值列表也是按图层对应的,每一项描述的是“这一层与它下面所有层合成后的结果怎么混合”。所以写linear-gradient(...), url(...)配background-blend-mode: overlay, normal,就是让渐变层以 overlay 方式融进图片。这个机制比半透明覆盖更细腻,但可控性差一些,效果需要反复试。

注意:background-blend-mode的作用域是整个背景层栈。如果你在同一个元素上还有第三层装饰渐变,它也会被卷进混合里。遇到意外结果,先数一下自己写了几个图层。

2.3 伪元素覆盖层:控制力最强的那条路

需要给渐变单独做动画、单独设opacity过渡、单独加filter的时候,多层背景就捉襟见肘了,因为那些属性作用在元素整体上,会影响图片。这时候用::after或者::before拉一层独立的盒子出来。

.cover { position: relative; overflow: hidden; } .cover::after { content: ""; position: absolute; inset: 0; background-image: linear-gradient(to top, rgba(0, 0, 0, .8) 0%, rgba(0, 0, 0, 0) 60%); pointer-events: none; transition: opacity .3s ease; } .cover:hover::after { opacity: .4; }

inset: 0是top/right/bottom/left四个方向都归零的简写,比写四行清楚。pointer-events: none必须加,否则这层伪元素会挡住下面所有链接和按钮的点击,这是我在评审中见得最多的失误。父元素要position: relative,内容层如果也做了绝对定位,记得把它抬到渐变层之上。

三种路线怎么选,我一般按这张表来判断:

方案代码量可控性性能适用场景
多层 background-image最少中好,单次绘制静态遮罩、文字可读性增强
background-blend-mode少中低好整图调色、氛围统一
伪元素覆盖层中等最高略差,多一层合成需要独立动画、hover 过渡、独立滤镜

我自己的默认策略是先上多层背景,等真的需要“渐变层单独动”的时候再重构到伪元素。不要一上来就堆伪元素,DOM 结构和层叠上下文都会变复杂,后期维护成本高。

3. 参数怎么调:角度、色标、透明色的实操推演

3.1 角度别靠猜,用关键词或者自己算

新手最容易犯的错是以为45deg就是“从左上到右下”。不是。45deg是从左下指向右上的一条线,颜色沿这条线插值。想让渐变正好沿对角线走,得用to bottom right这种角落关键字,浏览器会自动算出当前盒子比例下真正的对角角度。

如果一定要用数字,算法是这样:设容器宽w、高h,从左上角到右下角的方向向量是(w, h)。CSS 角度以正上方为零、顺时针增加,向上对应向量(0, -1),向右对应(1, 0),也就是 90°。于是目标角度等于90deg + atan(h / w)。举个实例,容器 320×180,atan(180/320) ≈ 29.4°,目标角度约119.4deg。同一个45deg在不同比例的容器里方向完全一样,但同一个to bottom right在宽扁容器和瘦高容器里角度完全不同——这就是关键字的好处,它跟着盒子走。

做图片暗化的时候,我基本只用to right、to bottom、to top这几个方向词,因为语义清楚,改的时候不用重算。

3.2 色标位置决定渐变“硬不硬”

色标后面跟百分比或者长度,控制这个颜色出现在渐变线的哪个位置。两个相邻色标之间是平滑过渡,两个色标写同一个位置就是一道硬边。这个特性可以做很实用的东西:比如图片底部只在下半部分渐隐,上半部分完全清晰,那就把透明色标全部压在 50% 之后。

linear-gradient(to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0) 45%, rgba(0, 0, 0, .85) 100%)

前两个色标位置不同但颜色相同,45% 之前就是纯透明。这种“平台 + 斜坡”的写法比单一斜坡更耐看,因为主体内容区域完全不受影响,只在底部压暗。

做卡片底部文字压暗时,我习惯把起点设在 40% 到 50% 之间,终点用rgba(0,0,0,.85)到.95。低于.6的话,浅色背景图上白字的对比度经常不够,实测在 4.5:1 这条线附近反复试探,还不如一次性压到位。对比度的算法是按相对亮度算的:(L1 + 0.05) / (L2 + 0.05),浅灰底上的白字很容易掉到 3:1 以下,肉眼看着“还行”,实际不达标。

3.3 transparent 这个关键字,能不用就不用

这是个老坑,但至今还有人踩。transparent在计算值上等于rgba(0, 0, 0, 0),也就是“透明的黑”。在渐变插值时,如果一端是透明黑、另一端是不透明的白,某些渲染路径下中间会经过灰色,视觉上就是一条脏兮兮的灰带,而不是干净的淡出。

现代浏览器在处理渐变时普遍采用了预乘 alpha 插值,这个问题的出现概率已经低了很多,但在混合模式叠加、mask-image、以及部分移动端内核上仍然可能冒出来。稳妥的做法是写“同色透明”——想让白色渐隐,就用rgba(255, 255, 255, 0);想让深蓝色渐隐,就用rgba(10, 20, 40, 0)。RGB 三个通道跟目标色保持一致,只把 alpha 归零,插值过程中就不会跑偏。

/* 容易发灰 */ linear-gradient(to bottom, transparent, #fff) /* 稳妥 */ linear-gradient(to bottom, rgba(255, 255, 255, 0), #fff)

同样的道理适用于mask-image。遮罩渐变里如果不是纯黑纯透明,边缘会出现半透明的灰边,看起来像脏了。

4. 完整做一个图片横幅:从结构到响应式

4.1 结构与基础样式

我拿一个常见的“左侧文字、右侧图片”的横幅来完整走一遍。HTML 结构保持极简,不要为了渐变多套一层没意义的div。

<section class="banner"> <div class="banner__inner"> <h2 class="banner__title">让每一张图都讲清楚一件事</h2> <p class="banner__desc">从素材整理到版面编排,把散落的视觉资产收进一套可复用的规范里。</p> <a class="banner__cta" href="#">看看怎么做</a> </div> </section>

容器本身承担背景图加渐变,内层负责排版。这样内层的padding变化不会影响背景的对齐,改文案也不用碰背景逻辑。

4.2 背景层的完整写法

.banner { position: relative; display: flex; align-items: center; min-height: clamp(360px, 42vw, 520px); padding: 48px clamp(20px, 5vw, 72px); border-radius: 16px; overflow: hidden; background-color: #101828; background-image: linear-gradient(100deg, rgba(16, 24, 40, .92) 0%, rgba(16, 24, 40, .72) 38%, rgba(16, 24, 40, .18) 68%, rgba(16, 24, 40, 0) 100%), url("banner.jpg"); background-size: cover, cover; background-position: center, center 30%; background-repeat: no-repeat, no-repeat; } .banner__inner { position: relative; z-index: 1; max-width: 44ch; color: #fff; }

background-position的第二项写成center 30%,是把图片的视觉重心往上挪一点。人像或者主体物通常在图片偏上位置,center center会把它裁掉一截,center 30%更稳。这个值我一般从 25% 到 35% 之间试,取决于素材。

border-radius配overflow: hidden是为了裁掉背景图超出圆角的部分。少了overflow: hidden,图片会从圆角外面露出来一角,非常明显。

min-height用clamp(360px, 42vw, 520px)而不是固定值,是因为横幅在平板竖屏下用固定高度会显得又扁又挤。42vw让它跟着视口宽度伸缩,clamp兜住上下限。这个组合我在好几个项目里用过,基本不用再补媒体查询。

4.3 响应式:小屏换图、大屏加码

小屏上的问题不是高度,是图片裁切。桌面端横幅是宽幅横图,手机上是竖长条,同一张background-size: cover会裁得只剩中间一点,主体全没了。比较稳的做法是准备两张图,用媒体查询切换,或者在支持image-set()的环境里交给浏览器按分辨率选。前者可控,后者省事但控制不了构图。

@media (max-width: 640px) { .banner { background-image: linear-gradient(to bottom, rgba(16, 24, 40, .55) 0%, rgba(16, 24, 40, .92) 62%, rgba(16, 24, 40, .97) 100%), url("banner-mobile.jpg"); background-position: center, center top; align-items: flex-end; padding-bottom: 40px; } }

注意小屏上渐变方向从100deg换成了to bottom。横屏版是左右分栏,渐变从左往右;竖屏版文字在下,渐变就得从下往上压暗。方向跟着布局走,不能一套参数打天下。

4.4 深色模式下的透明度重算

深色模式不是把颜色反转一下那么简单。同一个.92的黑色遮罩,在浅色主题下看起来自然,切到深色主题后图片会显得脏、发闷。我的调整方式是给透明度定义变量,主题切换时只改变量值。

.banner { --scrim-start: rgba(16, 24, 40, .92); --scrim-mid: rgba(16, 24, 40, .72); --scrim-end: rgba(16, 24, 40, 0); } @media (prefers-color-scheme: dark) { .banner { --scrim-start: rgba(6, 10, 18, .96); --scrim-mid: rgba(6, 10, 18, .55); --scrim-end: rgba(6, 10, 18, 0); } }

CSS 变量不能直接放进渐变函数里做插值动画,但静态使用完全没问题。好处是别人接手的时候,改色只需要动这三行,不用去渐变函数里翻那一长串数字。

5. 进阶玩法:流动渐变、遮罩渐隐、渐变文字

5.1 让渐变缓慢流动的正确姿势

渐变本身不能过渡,background-image是离散属性。能动的只有background-position或者background-size。常用套路是把背景尺寸放大到容器的两倍以上,然后让位置来回扫。

@keyframes drift { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } .aurora { background-image: linear-gradient(120deg, #3b82f6, #8b5cf6, #ec4899, #3b82f6); background-size: 300% 300%; animation: drift 18s ease-in-out infinite; }

background-size: 300% 300%是关键,尺寸不够大,位置移动时两边会露出空白。循环用的渐变首尾颜色必须一致(这里都是#3b82f6),否则接缝处会有明显的跳变。

实测下来,这个动画在中等尺寸的元素上很流畅,但铺满整个视口并且元素又很高的时候,会带来持续重绘。加will-change: background-position有帮助但不解决根本问题,因为背景位移动画走的是绘制路径,不是合成层。要做全屏流动效果,更稳的是用两个低透明度的伪元素做transform: translate加blur,让合成器接手。这条经验值不少钱,浏览器开发者工具里Rendering面板打开“Paint flashing”一眼就能看到区别。

5.2 mask-image:让图片边缘自然融进背景

要做“图片往下越来越淡直到消失”的效果,很多人第一反应还是在上面盖一层和页面背景同色的渐变。可行,但页面背景一变(换主题、换底色、图片上还有别的层)就得重调。mask-image更彻底,它按亮度裁掉透明度,不依赖父级背景色。

.thumb { mask-image: linear-gradient(to bottom, #000 55%, rgba(0, 0, 0, 0) 100%); mask-size: 100% 100%; mask-repeat: no-repeat; }

遮罩渐变里用纯黑到透明黑,黑色区域完全不透明,透明区域被裁掉。mask-size: 100% 100%是必须写的,不写的话遮罩图按元素尺寸以外的规则走,边缘会重复。

部分移动端内核对标准写法支持不完整,实际项目里通常两套都写一份,带厂商前缀的版本放在前面,标准属性放后面。这个前缀污染看着难受,但比在真机上发现看不到效果强。

5.3 渐变文字与渐变描边

渐变文字的写法很短,坑埋在细节里。

.grad-title { background-image: linear-gradient(90deg, #f97316, #ec4899); -webkit-background-clip: text; background-clip: text; color: transparent; }

三个条件缺一不可:背景要裁到文字上、文字颜色必须透明、背景图得存在。color: transparent少了的话什么也看不到,因为背景被文字盖住了。这个属性配合background-clip: text,在文字有换行、有text-shadow的时候会出问题,阴影会透出来变成色块,需要单独处理。

渐变描边相对麻烦一些,border-image能用但圆角就没法要了。目前比较实用的做法是双背景加内边距:外层放渐变当“边框”,内层用纯色背景盖住中间,靠padding留出边框宽度。麻烦,但兼容性稳。

6. 常见问题速查:从现象倒推原因

6.1 排查顺序:先数图层,再对尺寸

遇到“渐变没出来”“图片把渐变盖住了”,我的固定排查顺序是:第一,数background-image里有几个逗号分隔的图层;第二,数background-size和background-position是不是同样的数量;第三,确认颜色是不是写成了background-color(那只能有一个,而且永远在最底层)。

background-image: linear-gradient(...), red这种写法是无效的,颜色不是图像,浏览器会整条声明丢掉,看着就像什么都没生效。颜色只能放background-color,或者放background简写的最后一层。简写里颜色也只能出现一次,写在中途同样会让整条声明失效。

6.2 速查表

现象常见原因处理方式
渐变完全看不到图层顺序写反,图片在上;或颜色被写在background-image里渐变写第一个;颜色改到background-color
渐变只覆盖了一半background-repeat导致小尺寸图层平铺,或漏写background-size补background-repeat: no-repeat与对齐的background-size
渐变位置和图片错开background-size数量与图层数不一致,触发循环取值补齐数量,保证一一对应
渐变中间出现灰带用了transparent,插值经过透明黑换成同 RGB 的rgba(..., 0)
渐变有明显色阶断层大跨度低对比渐变,8bit 色深不够叠一层极低透明度的噪点,或改用更近的色相
遮罩在移动端失效只写了标准属性,未带厂商前缀补前缀版本,标准属性放后面
背景图在部分内核上不显示background-attachment: fixed与小屏滚动的兼容问题改用伪元素固定定位,或直接去掉 fixed
渐变层挡住了点击伪元素未设pointer-events: none补上该属性
圆角外露出背景图缺overflow: hidden在带border-radius的容器上补
打印时渐变丢失打印上下文默认不输出背景提示用户开启背景打印,或改由边框与图片承担信息

6.3 色带(banding)的处理手法

大面积的深色渐变最容易出色带,屏幕色深只有 8 位每通道,相邻色阶之间的跳跃在大面积上肉眼可辨。解决办法是加噪点打散边界。用一小段内联 SVG 生成噪点,以极低透明度铺在最上层。

.noise::after { content: ""; position: absolute; inset: 0; pointer-events: none; opacity: .04; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='140' height='140'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2'/></filter><rect width='140' height='140' filter='url(%23n)'/></svg>"); background-size: 140px 140px; }

opacity控制在.03到.06之间,超过这个范围噪点就肉眼可见了,变成“图片有颗粒感”而不是“渐变更平滑”。这段 SVG 里的#必须编码成%23,否则整个 data URI 会被当成片段标识截断,背景直接失效——这个问题我排查过整整一个下午。

7. 性能与资源:几个容易忽略的细节

7.1 图层数量和绘制成本

多层背景本身不会显著拖慢渲染,因为它们在一次绘制里就合成完了。真正贵的是大面积渐变叠加模糊、叠加混合模式、再加上动画。background-blend-mode在部分实现里需要额外的离屏合成,全屏使用时可以明显感觉到滚动帧率下滑。

我的经验阈值是这样:元素面积超过半个视口、且带持续动画时,慎用混合模式,改用预先生成好的调色图;静态情况下,三四层背景叠加完全没问题,不用过度优化。

7.2 图片资源如何选

background-size: cover的图片,浏览器不会替你做分辨率适配。在 2 倍屏上,一张 1200px 宽的图铺在 1200px 宽的容器里,实际会被拉伸到 2400 像素的物理像素上,肉眼看得出糊。解决办法是准备 2 倍图,或者用image-set()让浏览器按像素密度挑。

.banner { background-image: linear-gradient(...), image-set(url("banner@1x.jpg") 1x, url("banner@2x.jpg") 2x); }

写法比srcset略啰嗦,但收益一样明显。前提是渐变必须同样写在图层列表里,数量对齐这条规则在image-set()上依然生效。

7.3 一个容易被忽略的对齐细节

background-origin默认是padding-box,background-clip默认是border-box。这意味着渐变和图片的坐标起点是内边距边缘,但绘制区域延伸到边框外。容器有粗边框时,背景会在边框区域被拉伸,看起来像是错位了。给容器加background-origin: border-box,两层就都从边框外沿开始,对齐问题消失。这个细节在做带图示边框的卡片时特别有用,一行属性解决一小时的调试。

写到这里,回头看整件事其实就三句话:渐变是图不是色,图层顺序就是叠放顺序,透明色要用同色透明。把这三点吃透,剩下全是参数手感的问题。我个人的习惯是每做一个横幅就在开发者工具里把background-size和background-position的数值临时删掉再重写一遍,确认每一个列表项都对齐了再收工——多花三十秒,能省掉后面好几轮“为什么线上和本地不一样”的追问。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询