以前画个圆角渐变背景都要切图,现在 CSS 自己会画了——CSS Houdini Paint API 把我从图里彻底解放了
以前画个圆角渐变背景都要切图,现在 CSS 自己会画了——CSS Houdini Paint API 把我从图里彻底解放了
以前写 CSS 背景图,第一反应是打开 Figma 切个 2x 图。要渐变有条纹?那就 SVG。要动态条纹?那就 canvas 转 data URI。但这些方案有个共同问题——分辨率高了糊、容器大了要换图、想改个颜色得改完再重新导出。
CSS Paint API(也叫 CSS Houdini Paint)把这件事彻底变了。你可以在 CSS 里直接「画」出来,分辨率无关、容器自适应、颜色还能用 CSS 变量驱动——就像在浏览器里跑了一个 Canvas 画布,但用的是 CSS 语法。
它到底怎么工作的
Houdini Paint 的核心是一个叫 PaintWorklet 的东西。你写一段 JavaScript,用 registerPaint() 注册一个绘制函数,浏览器会在需要的时候调用它,传入一个 Canvas 2D 上下文让你画。
先建一个 worklet 文件 stripes.js:
// stripes.js
registerPaint(
"diagonal-stripes",
class {
static get contextOptions() {
return { alpha: true };
}
static get inputProperties() {
return ["--stripe-color", "--stripe-width"];
}
paint(ctx, size, props) {
const color = props.get("--stripe-color").toString().trim() || "#3b82f6";
const width = parseInt(props.get("--stripe-width")) || 20;
ctx.clearRect(0, 0, size.width, size.height);
ctx.fillStyle = color;
for (let i = -size.height; i < size.width + size.height; i += width * 2) {
ctx.beginPath();
ctx.moveTo(i, 0);
ctx.lineTo(i + size.height, size.height);
ctx.lineTo(i + size.height + width, size.height);
ctx.lineTo(i + width, 0);
ctx.closePath();
ctx.fill();
}
}
}
);
然后在主页面加载它,在 CSS 里用 paint() 函数调用:
// main.js
if ("paintWorklet" in CSS) {
CSS.paintWorklet.addModule("stripes.js");
}
.box {
background-image: paint(diagonal-stripes);
--stripe-color: #3b82f6;
--stripe-width: 20;
width: 400px;
height: 200px;
}
就这一行 background-image: paint(diagonal-stripes),浏览器就会执行你的 worklet,把图案画出来。把它改成 50px 宽,或者换个颜色,浏览器自动重绘——你不需要改 JS,不需要换图,它自己会变。
它和 Canvas 有什么不一样
最大的区别是:它受 CSS 驱动。paint() 是一个 CSS 函数,你可以在上面套媒体查询、套容器查询、套 :hover 伪类:
.card {
background-image: paint(corner-glow);
}
.card:hover {
--glow-intensity: 1;
}
@container (max-width: 400px) {
.card {
--glow-size: 60px;
}
}
同一个 paint() 函数,在不同状态下画不同的东西。传统方案你要么写一堆覆盖样式,要么用 JS 监听事件改 canvas——现在全扔给 CSS。
另外一点:分辨率自适应。Canvas 画出来是固定像素的,放大会糊。Paint API 每次重绘都基于当前容器的 size.width 和 size.height,所以不管容器多大,图案永远清晰。
实际能画哪些东西
这是我觉得最值钱的部分——以前必须靠图的,现在全可以用代码生成:
动态条纹背景(渐变、斑马纹、斜纹,一条 CSS 变量搞定)
registerPaint("gradient-stripes", class {
paint(ctx, size, props) {
const colors = ["#3b82f6", "#8b5cf6", "#ec4899"];
const stripeWidth = size.width / colors.length;
colors.forEach((color, i) => {
ctx.fillStyle = color;
ctx.fillRect(i * stripeWidth, 0, stripeWidth, size.height);
});
}
});
分辨率无关的棋盘格(Retina 屏再也不怕切图了)
registerPaint("checkerboard", class {
static get inputProperties() {
return ["--cell-size"];
}
paint(ctx, size, props) {
const cell = parseInt(props.get("--cell-size")) || 20;
for (let x = 0; x < size.width; x += cell) {
for (let y = 0; y < size.height; y += cell) {
ctx.fillStyle = (x / cell + y / cell) % 2 === 0 ? "#fff" : "#333";
ctx.fillRect(x, y, cell, cell);
}
}
}
});
复杂边框(圆角渐变边框、图案边框,不用 SVG)
.round-gradient-border {
border-image: paint(round-gradient-border);
border-width: 8px;
border-image-slice: 1;
}
动态光晕/阴影(替代多层的 box-shadow,颜色用 CSS 变量控制)
registerPaint("dynamic-glow", class {
static get inputProperties() {
return ["--glow-color", "--glow-radius"];
}
paint(ctx, size, props) {
const color = props.get("--glow-color")?.toString() || "rgba(59, 130, 246, 0.5)";
const radius = parseInt(props.get("--glow-radius")) || 20;
const gradient = ctx.createRadialGradient(
size.width / 2, size.height / 2, 0,
size.width / 2, size.height / 2, radius
);
gradient.addColorStop(0, color);
gradient.addColorStop(1, "transparent");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, size.width, size.height);
}
});
浏览器支持现在怎么样了
截至 2026 年,全球支持率约 63%。Chrome 65+、Firefox 102+、Safari 16.4+ 都支持。但它目前还没进 Baseline,所以生产环境用的时候要加 @supports 判断:
.gradient-bg {
background-image: linear-gradient(blue, purple); /* fallback */
}
@supports (background-image: paint(diagonal-stripes)) {
.gradient-bg {
background-image: paint(diagonal-stripes);
}
}
这个降级不是将就——你完全可以把 Paint API 当成增强层:基础体验用传统 CSS,想要复杂图案和动态效果再启用 worklet。移动端如果不支持,优雅退化成纯色或渐变,体验也不差。
调试是个坑,要先知道
PaintWorklet 运行在浏览器的独立 worklet 线程里,你在主线程打的 console.log 看不到,你在这个文件里断点也进不去。调试基本靠:
- 在 worklet 文件里自己加
console.log(会在浏览器控制台以Worklet前缀显示) - 用
CSS.paintWorklet.addModule()加载前加版本号做缓存清理:addModule("stripes.js?v=2") - 开发时用固定尺寸测试,因为 worklet 重绘时机和 CSS 解析时机不完全同步
下一步怎么上手
不要一开始就画复杂图案。先从最简单的开始:
- 建一个 HTML + 一个
worklet.js,用registerPaint画个纯色矩形 - 用
CSS.paintWorklet.addModule()加载,确认 worklet 跑通了 - 加
--custom-property,在 CSS 里改变量值看 worklet 重绘 - 再加
@container或@media,看不同条件下画不同的东西 - 最后加
@supports降级处理,上生产
这样循序渐进,你对这个「CSS 里跑 Canvas」的模型就建立手感了。
配图党、SVG 党,以前觉得必须靠设计工具做的事,现在 CSS 自己会画了。不需要切图、不需要转 data URI、不需要记一串 SVG 路径——写几行 JS,加一行 CSS,浏览器帮你画完了。
评论区
登录后可评论。