⭐ 推荐:社区规则条款 V1.0

CSS 混合模式 mix-blend-mode 创意效果合集

18565457544
18565457544 见习用户见习用户
发布于 2026-10-16 14:44 ·6 浏览 ·6 回复
内容摘要

通过区分 mix-blend-mode 与 background-blend-mode 的作用域,并掌握 multiply、screen、overlay、difference、luminosity 五种常用混合模式,可用少量 CSS 实现文字与图片交融、双色调封面、反色标题和白底图自动透明等效果,制作封面标题时需设置 isolation: isolate 避免与页面背景意外混合,双色调则通过叠加渐…

学会 mix-blend-mode,你能用几行 CSS 做出文字与图片交融、双色调封面、反色标题、白底图自动"抠图"这几类常见创意效果,而且不用开设计软件导出成品图。

第一步:分清两个属性,别一上来就写错

CSS 里跟混合有关的属性只有两个,作用域完全不同:

  • mix-blend-mode:控制一个元素和它背后已渲染的内容怎么混(包括父元素背景、兄弟元素、甚至页面背景)。
  • background-blend-mode:控制同一个元素的多重背景图之间怎么混,跟页面其它元素无关。

做个封面标题,用的是前者:

<div class="stage">
  <img src="cover.jpg" alt="">
  <h1 class="title">MIX</h1>
</div>
.stage { position: relative; isolation: isolate; }
.stage img { display: block; width: 100%; }
.title {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  margin: 0;
  color: #fff;
  font: 900 clamp(48px, 12vw, 160px)/1 system-ui;
  mix-blend-mode: overlay;
}

inset: 0 让标题铺满容器,place-items: center 把它居中——这两句比用 top: 50%; transform: translateY(-50%) 稳得多。

注意:.stage 上的 isolation: isolate 不是可选项。没有它,标题会穿过图片继续和 body 背景混合,页面一旦换背景色,效果就全变了。

第二步:常备五种混合模式,覆盖 90% 场景

不用背 16 种模式,先记住这五个:

模式效果典型用途
multiply正片叠底,白色变透明白底图标/Logo 融进浅色背景
screen滤色,黑色变透明黑底素材融进深色背景
overlay叠加,增强对比文字压在照片上、双色调
difference差值,反色反白标题、鼠标跟随反色
luminosity取亮度给图片上渐变色的"色罩"

白底图秒变透明:一张白底的 PNG Logo 放在浅灰卡片上,直接给它 mix-blend-mode: multiply,白底就消失了,比抠图快一万倍。

.logo { mix-blend-mode: multiply; }

反色标题,鼠标划过去像被"擦"出来:

.flip { color: #fff; mix-blend-mode: difference; }

注意:difference 的结果取决于底下那一层的实际颜色,深色底会得到亮色字,浅色底得到暗色字。文字可读性必须实机看,别只信设计稿。

第三步:双色调封面(Duotone)

给照片叠一层渐变,用 mix-blend-mode 混进去:

.duo { position: relative; isolation: isolate; }
.duo img { display: block; width: 100%; filter: grayscale(1) contrast(1.15); }
.duo::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(45deg, #ff2d55, #5856d6);
  mix-blend-mode: multiply;
}

关键是先把图片去色(grayscale(1)),否则叠加后颜色会脏。想要更亮的氛围,把 multiply 换成 screen 或 overlay 各试一次,选顺眼的那个。

第四步:给背景加质感纹理

多重背景之间的混合,用 background-blend-mode:

.grain {
  background-image:
    url("noise.png"),
    linear-gradient(135deg, #1a1a2e, #16213e);
  background-blend-mode: overlay, normal;
  background-size: 200px, cover;
}

列表里写几个模式,就对应几张图;写一个模式则所有层都用同一种。顺序是从上到下对应背景图的先后,别搞反。

第五步:控制混合范围与性能

想让某个区域整体不跟外部混,给它加 isolation: isolate;反过来,如果你发现混合"没生效",先查这条链路上有没有元素偷偷创建了堆叠上下文——transform、opacity 小于 1、filter、will-change、position: fixed 都会。

性能上,mix-blend-mode 会让浏览器多做一次合成。一个页面用三五处没问题,但如果一个滚动长列表里每个卡片都套一层,低端手机上滚动会明显发涩。

兜底写法:

@supports not (mix-blend-mode: overlay) {
  .title { color: #fff; text-shadow: 0 2px 12px rgba(0,0,0,.6); }
}

注意:调试时直接在 DevTools 的 Elements 面板改 mix-blend-mode 的值,实时看效果最快;如果结果和预期不符,用 Layers 面板看看到底哪些元素被提升成了合成层。

小结

  • mix-blend-mode 管元素与背后内容的混合,background-blend-mode 管同一元素的多重背景,两者别混用。
  • 记住 multiply(白变透)、screen(黑变透)、overlay(增强对比)、difference(反色)、luminosity(上色)五个就够起步。
  • 容器加 isolation: isolate,把混合关在一个盒子里,效果才可控。
  • 双色调先 grayscale(1) 再叠色,颜色才不会脏。
  • 混合层会触发合成,长列表里别大面积使用,并用 @supports 给不支持的浏览器留后路。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-849.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 6

xiaojiu
xiaojiu 正式会员正式会员认证极客认证极客 1楼 2026-10-16 14:51

思路和分层完全对,但真正会卡人的不是选哪个模式,而是「混合范围被悄悄截断」和「导出成图后效果蒸发」这两件事。

先补一句 isolation: isolate 的延伸:任何会生成层叠上下文的属性都会隐式隔离混合,包括祖先元素上的 transform、opacity < 1、filter、will-change: transform、backdrop-filter。所以你单独抽出去看没问题,一塞进业务组件就失效——多半是外面某个 transition: transform .3s 或骨架屏的 opacity: .6 在作祟。建议把混合效果做成自包含组件(自己带一层 isolation 容器),别依赖页面结构,否则别人改个动画就把你干掉了。局部调试最土也最快的办法:临时给容器加 outline: 1px solid red,一眼看出混合到哪一层为止。

第三步的双色调(你贴的代码断在这)补完整,两伪元素一压一提就够:图片先 filter: grayscale(1) contrast(1.05),然后 ::before 用深色配 mix-blend-mode: multiply 压亮部,::after 用浅色配 screen 提暗部。想要某一种配色,改这两层颜色即可,不用动原图。

性能上留个心:混合模式走 GPU 合成,长列表、卡片流里大面积铺会掉帧,尤其是和滚动监听、position: fixed 一起用时;Safari 对 fixed 元素的混合也有已知的怪异表现,别在吸顶导航上滥用。

最后一个坑:difference 的对比度是不可控的,正文标题别用它,必须实机测;

fanrenxiuxian
fanrenxiuxian 正式会员正式会员认证极客认证极客 #814 2楼 2026-10-16 14:57
xiaojiu:思路和分层完全对,但真正会卡人的不是选哪个模式,而是「混合范围被悄悄截断」和「导出成图后效果蒸发」这两件事。 先补一句 `isolation: isolate…

你补的这两点才是真坑——模式选择十分钟就能上手,剩下全是跟层叠上下文和导出管线耗。

先把「隐式隔离」的清单补全:除了你列的 transform / opacity < 1 / filter / backdrop-filter / will-change,clip-path、mask、perspective、contain: layout|paint、以及祖先的 position: fixed 同样会生成层叠上下文(注意 position: relative + z-index 也算)。所以别去记清单,记判据:Chrome DevTools 的 Elements 面板上会直接给生成层叠上下文的节点打标记,顺着往上找第一个带标记的祖先就是"截断点"。你说的 outline: 1px solid red 配合这个标记,基本一次定位。组件自包含这个建议我举双手赞成,唯一例外是"要让标题和页面背景混"那种设计,那就得反过来,故意不隔离——这个取舍最好写进注释,不然半年后自己都忘了为什么。

双色调那段顺序我建议微调:grayscale 放 img 上没问题,但两个伪元素一定要落在同一个 isolation: isolate 容器里,否则 screen 那层会继续和页面底色混,浅色页看起来"发灰"。另外 ::after 的 screen 建议配 opacity: .6~.8,不然暗部提得过头,肤色会发脏。

导出成图这块补一句:html2canvas / foreignObject 那条路对 mix-blend-mode 基本没戏,需要导出就走两条路——① canvas 2D 的 globalCompositeOperation 支持 multiply/screen/overlay/difference/luminosity 全套,手动 drawImage 两次即可;② 构建期或服务端用 sharp/ImageMagick 把双色调预先合成为静态图,前端只负责展示。做分享海报的选 ② 更省心。

性能我同意你的判断,再补一个体感最差的组合:混合元素上挂了 hover 变色 + transition,鼠标划过会反复触发重合成,卡片流里能明显看到模糊一帧。

最后那个 difference 的坑,我的土办法是把它只用在纯色底上——颜色一旦确定,对比度就是可预测的,用在照片上纯属赌运气。

laoxie
laoxie 正式会员正式会员认证极客认证极客 #815 3楼 2026-10-16 15:02
fanrenxiuxian:你补的这两点才是真坑——模式选择十分钟就能上手,剩下全是跟层叠上下文和导出管线耗。 先把「隐式隔离」的清单补全:除了你列的 `transform` / `op…

判据比清单靠谱这点我同意,但 DevTools 那个标记有个前提得说清:Chrome 只在部分版本给生成层叠上下文的节点打徽标,Safari 和 Firefox 基本没有,团队里换台机器就不灵。要跨浏览器一次定位,我一般直接在 Console 里往上爬:

const findBreak = el => {
  for (let n = el.parentElement; n !== document.documentElement; n = n.parentElement) {
    const s = getComputedStyle(n);
    if (s.isolation === 'isolate' || s.transform !== 'none' || +s.opacity < 1 ||
        s.filter !== 'none' || s.backdropFilter !== 'none' ||
        /transform|opacity|filter/.test(s.willChange) ||
        /paint|layout/.test(s.contain) ||
        ['fixed','sticky'].includes(s.position)) return n;
  }
  return null;
};
findBreak(document.querySelector('.title'));

第一个返回的就是「截断点」,比顺着徽标肉眼找快得多。这版没覆盖 perspective / mask / clip-path,按需加条件就行。

ipzh
ipzh 正式会员正式会员认证极客认证极客 钻石卡会员钻石卡会员 ipzh.com域名网 Lv3 #816 4楼 2026-10-16 15:09
laoxie:判据比清单靠谱这点我同意,但 DevTools 那个标记有个前提得说清:Chrome 只在部分版本给生成层叠上下文的节点打徽标,Safari 和 Firefox…

脚本方向对,可以直接抄去用,但 backdropFilter 那一行在不支持该属性的浏览器上会 100% 误报——s.backdropFilter 返回 undefined,而 undefined !== 'none' 是 true,于是第一个祖先就被判定成截断点,顺着它排查会走进死胡同。改成 s.backdropFilter && s.backdropFilter !== 'none',老 Safari 还得再加个 s.webkitBackdropFilter 兜底。同一条链上 s.contain、s.willChange 是安全的:undefined 会被正则转成字符串 "undefined",+s.opacity 是 NaN < 1 为 false,都不会误伤。

另外它是静态快照,这恰恰对应 xiaojiu 说的"一塞进组件就失效":真正在捣乱的往往不是常态的 transform,而是 transition: transform .3s 过渡中途那一刻生成的临时层叠上下文。过渡结束后再跑,脚本抓不到;过渡中跑,又会把正在动的元素报成截断点。所以建议加一步单独排查:往上顺的时候顺手看 s.transitionProperty 和 s.animationName,命中 transform / opacity / filter 就标红,人眼再判断。.stage 这类骨架屏的 opacity: .6 通常就是这么来的。

起点用 parentElement、不查自身这点是对的,元素自己的 transform / isolation 不影响它和背后内容的混合(混合对象是它所在的父层叠上下文),这点不用改。

wbcm
wbcm 见习用户见习用户 #817 5楼 2026-10-16 15:15
ipzh:脚本方向对,可以直接抄去用,但 `backdropFilter` 那一行在不支持该属性的浏览器上会 100% 误报——`s.backdropFilter` 返回…

backdropFilter 那处误报是实打实的 bug,你的 s.backdropFilter && ... 写法没问题;再省事一点可以用 'backdropFilter' in s 做特性分支,或者统一收一个 const on = v => !!v && v !== 'none',把 filter / backdropFilter(以及将来要加的 mask、webkitMask)都套进去,免得下次在同一个坑上再栽一次。

过渡中途那个"幽灵层叠上下文",我建议别靠读 s.transitionProperty,改用 getAnimations(),它拿的是引擎当前真实在跑的东西:

const busy = n => n.getAnimations().some(a =>
  a instanceof CSSTransition && /transform|opacity|filter/.test(a.transitionProperty)
);

CSSAnimation 同理能捞正在跑的 keyframes。关键优势是 transition: all 这种写法——computed 里你只能读到 all,照样得人猜,而 CSSTransition.transitionProperty 给的是具体属性名,直接能判。我自己的用法是分两趟:静态爬一遍拿截断点,再在触发交互的瞬间跑一次 busy 版,两次结果一对比,谁在过渡中就明牌了。

还有个同类盲区:will-change 有时是悬停/滚动时 JS 临时加的(el.style.willChange = 'transform'),跑脚本的时机不对一样抓瞎。所以这套脚本的正确定位是"缩小范围",最终定性还得配 DevTools 的 Animations 面板逐帧看。

起点用 parentElement 这条我也同意:元素自身的 filter 作用在混合之后,查自身反而会误导。

1717左
1717左 见习用户见习用户 #818 6楼 2026-10-16 15:20
wbcm:`backdropFilter` 那处误报是实打实的 bug,你的 `s.backdropFilter && ...` 写法没问题;再省事一点可以用 `'bac…

两处都认同,on 那个小工具不只是省字——它顺手把 '' 和 undefined 一起兜了,有些引擎在 filter 未设时返回空串而不是 none,单写 v !== 'none' 一样会误报,你这版写法更硬。

getAnimations() 比我原来读 s.transitionProperty 的思路高一档,transition: all 那个场景 computed 确实拿不到具体属性名。补两个落地细节:一是 getAnimations() 只收元素自身的动画,伪元素上的得走 getComputedStyle(el, '::after').animationName 单独看,双色调那两个伪元素正好踩这条;二是别在交互触发的同一个 tick 里读,那时样式还没重算,transition 可能压根没建出来。挂事件更稳:

el.addEventListener('transitionrun', e => { /* 此刻 getAnimations() 必能看到 */ });

transitionrun 在延迟阶段之前就触发,比 transitionstart 早,用来抓"幽灵层叠上下文"正合适,也省掉了你分两趟时的时机猜测。

will-change 那条也提醒我了:静态趟如果跑在 JS 挂上之前,同样抓瞎。我的做法是把 busy 版绑在 pointerenter / scroll 上,用 requestAnimationFrame 延后一帧执行,否则读到的还是上一帧的状态。

同意你这套脚本的定位——负责缩圈,定性还得回到 Animations 面板逐帧看。唯一想加一句:这类探测别当常驻钩子挂着,getAnimations() 每次调用都要保证样式是新的,滚动里高频跑容易反复触发样式计算,排查完就摘掉。