网页的默认形状只有一个:矩形。而人的眼睛天生对矩形缺乏兴趣。过去我们要在页面里做出一个六边形、一个水滴、一个环形、一段让文字绕着走的曲线,只能导出 SVG 或 PNG——图片一旦响应式缩放就会模糊,颜色一改就得重新导出,主题切换更是灾难。现在,CSS 自己就能画出这些形状,而且能动画、能响应式、能跟随主题变量实时变化。本篇 40 分钟长文,从 clip-path 的四种基本图形讲到可拖拽的多边形编辑器,从 border-radius 的八值语法讲到有机形状的数学,从 shape-outside 的文字环绕讲到 mask 与 mask-composite 的图层运算,最后落到渐变驱动的创意图形、文字裁剪、响应式坐标系、渲染性能与无障碍。文中嵌入了六个可以亲手拨动的实验台,建议你边读边改——形状这件事,看一百遍不如拖一次控制点。
为什么“矩形时代”正在结束
在 Web 的早期,矩形是唯一的选择,因为盒模型就是矩形。所有元素都是一个盒子,盒子可以加边框、可以加圆角、可以加阴影,但它的轮廓永远是矩形(最多是圆角矩形)。想要别的形状,只能靠图片或者 SVG。
这套方案有三个无法回避的问题:
- 不可响应:位图放大就模糊,缩小就浪费带宽。
- 不可主题化:深色模式要重新导出一套图,品牌色一变就要全量替换。
- 不可交互:图片的透明区域仍然会接收点击,用户点到“空气”上也能触发事件。
CSS 形状能力成熟之后,这三个问题同时被解决了。形状变成了样式的一部分:它跟随 CSS 变量,跟随媒体查询,跟随伪类状态,能被 transition 平滑过渡,能被 DevTools 实时调试,而且体积是零。
/* ① 位图 PNG */
.logo { background: url(logo.png); }
/* 缺点:模糊、体积大、不可换色 */
/* ② 内联 SVG */
<svg viewBox="0 0 100 100">
<polygon points="50,0 100,38 82,100 18,100 0,38"/>
</svg>
/* 优点:矢量、可控;缺点:DOM 变重 */
/* ③ 纯 CSS 形状(本文主角) */
.logo {
background: var(--brand);
clip-path: polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%);
}
/* 优点:零体积、可动画、可换色、可响应式 */
一个关键的认知转变
很多开发者第一次接触 clip-path 时会问:“被裁掉的部分还能被点击吗?”答案是:不能。这是一个巨大的优点。用 clip-path 裁出来的圆形按钮,只有圆形区域可点击,四个角是彻底“透明”的。用图片做同样的事情,你必须写额外的 JS 去判断点击坐标——而 CSS 免费帮你做了。
clip-path 快速上手:四种基本图形
clip-path 接受一个“基本图形函数”作为值。最常用的有五个:inset()、circle()、ellipse()、polygon()、path()。前四个都支持百分比,会随元素尺寸自动缩放;path() 使用的是 SVG 路径语法,坐标是绝对数值,响应式能力最弱,除非你配合 viewBox 思路去做换算。
.a { clip-path: inset(10% 20% 10% 20%); }
.b { clip-path: inset(10% round 24px); }
.c { clip-path: inset(0 0 30% 0); } /* 底部切掉 30% */
/* ② circle() —— 圆形,语法:circle(半径 at 圆心) */
.d { clip-path: circle(50%); }
.e { clip-path: circle(45% at 30% 30%); }
.f { clip-path: circle(closest-side at 50% 50%); }
/* ③ ellipse() —— 椭圆,语法:ellipse(横半径 纵半径 at 圆心) */
.g { clip-path: ellipse(50% 35% at 50% 50%); }
.h { clip-path: ellipse(70% 30% at 50% 70%); }
/* ④ polygon() —— 任意多边形,最强大也最常用 */
.i { clip-path: polygon(50% 0%, 100% 100%, 0% 100%); }
.j { clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); }
/* ⑤ path() —— SVG 路径语法,坐标是绝对值 */
.k { clip-path: path('M 0 0 L 100 0 L 50 100 Z'); }
/* 也可以用 url() 引用 SVG 的 clipPath 元素 */
.l { clip-path: url(#myClipPath); }
点击下面的按钮,切换同一个方块的不同裁剪形状。注意观察:方块本身的位置、大小、阴影都没有变化,变的只是“哪一部分可见”。这是理解 clip-path 最关键的一点。
关于坐标系:一切以元素自身的盒子为基准
clip-path 的百分比坐标是相对于元素自身的边框盒(border-box)计算的。左上角是 0% 0%,右下角是 100% 100%。这意味着:
- 元素变宽变高,形状会等比拉伸,而不是保持原始比例。
- 如果元素是
200px × 100px的横向矩形,一个“正三角形”也会被拉扁。 - 想要形状保持比例,需要配合
aspect-ratio: 1或者用padding-top技巧锁定宽高比。
.avatar-shape {
width: 100%;
aspect-ratio: 1 / 1;
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
}
/* 或者用 CSS 变量让形状比例可调 */
.shape {
--w: 50%;
--h: 30%;
clip-path: ellipse(var(--w) var(--h) at 50% 50%);
}
polygon() 深入:用鼠标亲手捏一个形状
polygon() 之所以强大,是因为它把“形状”还原成了最朴素的数据结构:一串坐标点。想画什么形状,就列出它的顶点,浏览器会自动把首尾连起来并填充。下面这个编辑器把多边形的每个顶点都变成了可拖拽的小圆点——拖动它们,你会立刻看到形状和代码同步变化。
动手拖几次之后,你会形成一种直觉:顺时针排列顶点、点与点之间不要交叉、坐标用百分比。这三点覆盖了 90% 的实际使用场景。
/* 三角形 */
clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
/* 倒三角(气泡尾巴常用) */
clip-path: polygon(0% 0%, 100% 0%, 50% 100%);
/* 平行四边形(倾斜卡片) */
clip-path: polygon(15% 0%, 100% 0%, 85% 100%, 0% 100%);
/* 梯形 */
clip-path: polygon(20% 0%, 80% 0%, 100% 100%, 0% 100%);
/* 菱形 */
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
/* 右箭头 */
clip-path: polygon(0% 20%, 60% 20%, 60% 0%, 100% 50%, 60% 100%, 60% 80%, 0% 80%);
/* 斜切角卡片(右上角被切掉) */
clip-path: polygon(0% 0%, calc(100% - 24px) 0%, 100% 24px, 100% 100%, 0% 100%);
/* 消息气泡(带尖角) */
clip-path: polygon(0% 0%, 100% 0%, 100% 75%, 20% 75%, 12% 100%, 14% 75%, 0% 75%);
px 与 % 可以写在同一个 polygon 里
calc(100% - 24px) 能精确切角
当形状“切掉”了内容怎么办
这是一个高频问题:给卡片加了斜切角之后,右上角的关闭按钮不见了。原因是被裁掉的部分不只是不可见,而是完全不存在——它不渲染、不响应事件、不参与可访问性树的可视范围。
解决办法是:把内容放进一个内层容器,给内层容器留出足够的 padding,让内容始终位于可见区域内。或者更优雅一点,用 clip-path 只裁背景层,内容层不裁。
<div class="card-clip">
<div class="card-clip__bg"></div>
<div class="card-clip__body">内容</div>
</div>
.card-clip { position: relative; }
.card-clip__bg {
position: absolute;
inset: 0;
background: linear-gradient(135deg, #2563eb, #7c3aed);
clip-path: polygon(0 0, calc(100% - 32px) 0, 100% 32px, 100% 100%, 0 100%);
}
.card-clip__body {
position: relative;
padding: 24px;
padding-top: 40px; /* 给切角让位 */
}
让形状动起来:过渡、变形与 @property
形状能动画,是 CSS 形状真正的杀手锏。但这里有一条铁律:两个 clip-path 只有在“形状类型相同、顶点数量相同”时才能平滑插值。
| 变化 | 能否过渡 | 说明 |
|---|---|---|
circle(30%) → circle(60%) |
可以 | 半径直接插值,最平滑 |
circle(40% at 30% 30%) → circle(40% at 70% 70%) |
可以 | 圆心位置也会插值 |
inset(10%) → inset(30% 20%) |
可以 | 四个方向分别插值 |
| 5 点 polygon → 5 点 polygon | 可以 | 逐点插值,点顺序要对应 |
| 5 点 polygon → 6 点 polygon | 不行 | 点数不一致,直接跳变 |
circle() → polygon() |
不行 | 形状类型不同,无法插值 |
.shape {
clip-path: polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%);
transition: clip-path 0.6s cubic-bezier(0.2, 0, 0, 1);
}
.shape:hover {
clip-path: polygon(50% 8%, 92% 50%, 70% 92%, 30% 92%, 8% 50%);
}
/* ✅ 用 keyframes 做持续的形状呼吸 */
@keyframes morph {
0%, 100% {
clip-path: polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%);
}
50% {
clip-path: polygon(50% 6%, 94% 50%, 72% 94%, 28% 94%, 6% 50%);
}
}
/* ❌ 点数不一致,会瞬间跳变 */
.bad {
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); /* 4 点 */
transition: clip-path 0.5s;
}
.bad:hover {
clip-path: polygon(50% 0%, 100% 38%, 82% 100%, 18% 100%, 0% 38%); /* 5 点 */
}
五边形 ↔ 更饱满的五边形,6 秒一个来回
@property:让自定义属性也能参与插值
如果你把形状的某个参数抽成 CSS 变量,默认情况下浏览器不知道它是什么类型,因此不会对它做平滑插值——变化会瞬间跳变。@property 就是用来声明变量类型的。
@property --shape-angle {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
/* 声明一个百分比类型 */
@property --blob-radius {
syntax: '<percentage>';
initial-value: 30%;
inherits: false;
}
.blob {
--blob-radius: 30%;
border-radius: var(--blob-radius) 70% 70% var(--blob-radius) / var(--blob-radius) var(--blob-radius) 70% 70%;
transition: --blob-radius 0.6s ease;
}
.blob:hover { --blob-radius: 50%; }
注意:@property 目前在所有现代浏览器中都可用,但如果你必须支持较老的 Safari,退化方案是把参数直接写进 clip-path / border-radius,用两组完整值做过渡。
border-radius 的八值语法:不用 clip-path 也能造有机形状
大多数人对 border-radius 的认识停留在“圆角”。但它的完整语法其实能做出叶片、水滴、花瓣、蛋形等一系列有机形状,而且不需要 clip-path,兼容性极好,还自动保留边框与阴影。
border-radius: 左上 右上 右下 左下 / 左上 右上 右下 左下;
/* 斜杠前是“横向半径”,斜杠后是“纵向半径” */
.leaf {
border-radius: 0% 100% 0% 100% / 0% 100% 0% 100%;
}
.egg {
border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
}
.petal {
border-radius: 70% 30% 70% 30% / 30% 70% 30% 70%;
}
/* 经典 blob:不对称的四个角 */
.blob {
border-radius: 63% 37% 54% 46% / 55% 48% 52% 45%;
}
下面这个实验台把八个半径全部暴露出来。拖动滑块,你可以直观看到“横向半径”与“纵向半径”各自控制什么。当两者差异越大,形状就越“歪”,越有手工感。
让 blob 自己呼吸
有机形状最适合做“缓慢呼吸”的循环动画。关键技巧是:在关键帧之间改变 border-radius 的百分比,让形状像液体一样流动。
0%, 100% {
border-radius: 63% 37% 54% 46% / 55% 48% 52% 45%;
}
33% {
border-radius: 40% 60% 45% 55% / 60% 40% 60% 40%;
}
66% {
border-radius: 55% 45% 62% 38% / 42% 58% 42% 58%;
}
}
.blob {
animation: blobBreath 9s ease-in-out infinite;
will-change: border-radius;
}
/* 无障碍:减少动态时停掉呼吸 */
@media (prefers-reduced-motion: reduce) {
.blob { animation: none; }
}
需要提醒的是:border-radius 的动画会触发重绘(Paint),比纯合成动画贵一些。所以别在一屏里放几十个同时在呼吸的 blob——两三个点缀足矣。
shape-outside:让文字绕着形状流动
clip-path 改变的是元素自己的可见轮廓;shape-outside 改变的是周围文字环绕它的方式。后者常被忽略,但它是做出“杂志感排版”的关键。
基本规则只有三条:
- 元素必须是浮动元素(
float: left/right),否则shape-outside不生效。 - 元素必须有宽度和高度,否则没有可供环绕的轮廓。
shape-outside只影响环绕,不影响视觉。想让视觉也变成那个形状,需要再加一个同值的clip-path。
.float-avatar {
float: left;
width: 150px;
height: 150px;
margin: 6px 20px 10px 0;
shape-outside: circle(50% at 50% 50%);
clip-path: circle(50% at 50% 50%);
}
/* 可用的形状函数与 clip-path 一致 */
shape-outside: circle(50%);
shape-outside: ellipse(50% 40% at 50% 50%);
shape-outside: inset(10% 20% round 20px);
shape-outside: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
/* 用图片的 alpha 通道做环绕 */
.float-img {
float: left;
shape-outside: url(avatar.png);
shape-image-threshold: 0.5; /* alpha 阈值 */
}
/* 用渐变做环绕(渐变透明的地方文字可以压上去) */
.float-grad {
float: left;
shape-outside: linear-gradient(45deg, #000 40%, transparent 60%);
}
/* 环绕元素之间留一点安全距离 */
.float-avatar { shape-margin: 12px; }
点击下面的按钮切换环绕形状,观察左侧色块周围文字的重排。注意:文字的行高没有变化,只是每一行能放下多少字变了。
CSS 形状让排版第一次拥有了杂志般的自由度。文字不再只能沿着一个方方正正的边界流动,它可以贴合圆形、椭圆、多边形,甚至是图片的透明轮廓。当一段文字围绕一个圆形头像自然收拢时,读者的视线会被温柔地引导,而不是被生硬的直角打断。
需要注意的是,shape-outside 只对浮动元素生效,这是它最常被踩的坑。此外,形状之外的空间虽然在视觉上被“让开”,但元素本身仍然占据完整的盒子空间,因此布局计算不会因此改变——改变的只有文字的行盒内容宽度。
对于中文排版,圆形与椭圆环绕的观感通常最好,多边形的锐角容易让文字出现孤字或者行尾参差。如果你的排版要求严格的对齐感,建议使用较为圆润的形状,或者配合 shape-margin 留出足够的缓冲。
shape-outside 的三个实战场景
- 人物介绍卡片:头像用圆形环绕,正文自然贴合弧形,比传统的“头像 + 右侧文字”更有呼吸感。
- 首字下沉:把首字母做成一个浮动的大字,用
shape-outside: polygon()让后文紧贴字形轮廓。 - 杂志式图文混排:图片用不规则的斜切多边形环绕,营造出“剪贴簿”的视觉效果。
最后提醒一句:shape-outside 会影响文字的行盒计算,触发布局(Layout)。因此不要用它做动画——形状一旦在动画中变化,每一帧都要重新排版整段文字,代价极高。静态使用,才是它的最佳位置。
mask 遮罩:比 clip-path 更细腻的可见性控制
clip-path 的边界是硬的:形状之内全可见,之外全不可见。而 mask 的边界是软的:它用一张“遮罩图”的明度或透明度来决定每个像素的可见程度。这意味着它可以做渐变淡出、可以做纹理镂空、可以做图层运算。
.fade-bottom {
-webkit-mask-image: linear-gradient(to bottom, #000 60%, transparent 100%);
mask-image: linear-gradient(to bottom, #000 60%, transparent 100%);
}
/* 用重复渐变做条纹镂空 */
.stripes {
mask-image: repeating-linear-gradient( 45deg, #000 0 14px, transparent 14px 28px);
}
/* 用径向渐变做环形 */
.ring {
mask-image: radial-gradient(circle, transparent 38%, #000 40%);
}
/* 用图片的亮度做遮罩 */
.photo-mask {
mask-image: url(mask.png);
mask-size: cover;
mask-position: center;
mask-repeat: no-repeat;
}
/* 图层运算:多个遮罩相加、相交、相减 */
.venn {
mask-image: radial-gradient(circle at 30% 50%, #000 40%, transparent 41%), radial-gradient(circle at 70% 50%, #000 40%, transparent 41%);
mask-composite: add; /* 并集 */
}
.venn-intersect {
mask-image: radial-gradient(circle at 30% 50%, #000 40%, transparent 41%), radial-gradient(circle at 70% 50%, #000 40%, transparent 41%);
mask-composite: intersect; /* 交集 */
}
.venn-subtract {
mask-image: radial-gradient(circle at 30% 50%, #000 40%, transparent 41%), radial-gradient(circle at 70% 50%, #000 40%, transparent 41%);
mask-composite: subtract; /* 差集 */
}
拖动下面的滑块改变遮罩角度,你会看到彩色渐变被切成了不断旋转的条纹。背景没有变,变的只是“哪些像素被允许显示”。
用一条 radial-gradient 就能把实心圆变成圆环
mask 与 clip-path 的选择
| 维度 | clip-path | mask |
|---|---|---|
| 边界类型 | 硬边(锯齿可通过抗锯齿缓解) | 软边,支持渐变过渡 |
| 是否支持渐变 | 否 | 是 |
| 是否支持图片遮罩 | 只能通过 url() 引用 SVG |
是,任意图片 |
| 图层运算 | 不支持 | mask-composite 支持四种运算 |
| 典型用途 | 几何形状、图标、卡片切角 | 淡出、纹理、镂空、复杂合成 |
简单记:要“切成什么形状”用 clip-path,要“渐渐消失”用 mask。
渐变驱动的创意形状:不用裁剪也能造图形
有些“形状”其实不需要裁剪,只需要一个合适的渐变。比如纯 CSS 的三角形、扇形、环形、波浪、条纹背景,本质上都是渐变在特定尺寸下的视觉结果。
.pie-25 {
border-radius: 50%;
background: conic-gradient(#2563eb 0 25%, #e2e8f0 25% 100%);
}
/* 多段环形进度 */
.donut {
border-radius: 50%;
background: conic-gradient( #2563eb 0 40%, #8b5cf6 40% 70%, #f59e0b 70% 90%, #e2e8f0 90% 100%);
-webkit-mask: radial-gradient(circle, transparent 55%, #000 57%);
mask: radial-gradient(circle, transparent 55%, #000 57%);
}
/* 纯 CSS 三角形 */
.triangle {
width: 0;
height: 0;
border-left: 50px solid transparent;
border-right: 50px solid transparent;
border-bottom: 86px solid #2563eb;
}
/* 波浪分隔线 */
.wave {
height: 80px;
background: radial-gradient(circle at 50% 0, transparent 24px, #2563eb 25px);
background-size: 60px 100%;
}
/* 斜向条纹 */
.stripes {
background: repeating-linear-gradient( 45deg, #2563eb 0 12px, #60a5fa 12px 24px);
}
/* 用 conic-gradient 做扇形遮罩 */
.fan {
-webkit-mask: conic-gradient(from 0deg, #000 0 70%, transparent 70%);
mask: conic-gradient(from 0deg, #000 0 70%, transparent 70%);
}
渐变 + 裁剪 + 圆角,三件套可以组合出无穷多的图形
conic-gradient 是被低估的形状工具
很多人只把 conic-gradient 当作画饼图的手段,但它其实是做扇形、雷达、扇形遮罩、扇形高光的最佳工具。当它和 mask 结合时,可以精确控制“从哪个角度开始可见、到哪个角度结束”。
@property --sweep {
syntax: '<angle>';
initial-value: 0deg;
inherits: false;
}
.sweep {
--sweep: 0deg;
mask: conic-gradient(from 0deg, #000 var(--sweep), transparent var(--sweep));
animation: sweepIn 1.2s ease-out forwards;
}
@keyframes sweepIn {
to { --sweep: 360deg; }
}
配合 @property 声明角度类型,扇形扫描动画就成了一段纯 CSS 的“雷达扫描”效果,全程不需要 JavaScript,也不占用主线程。
文字裁剪:background-clip: text 的无限可能
把 background-clip 设为 text,背景就被裁成了文字的形状。文字本身变成透明,露出背后的渐变、图片、甚至视频。这是纯 CSS 做出“质感标题”最直接的方式。
.gradient-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 7s linear infinite;
}
@keyframes textFlow {
to { background-position: 300% 0; }
}
/* 图片填充文字 */
.photo-text {
background: url(photo.jpg) center / cover;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
/* 描边文字:用 -webkit-text-stroke */
.outline-text {
color: transparent;
-webkit-text-stroke: 2px #2563eb;
}
/* 渐变描边 + 渐变填充 */
.duo-text {
background: linear-gradient(135deg, #2563eb, #ec4899);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-stroke: 1px rgba(255,255,255,0.6);
}
/* 用 mask 给文字做渐隐(比 background-clip 更灵活) */
.fade-text {
mask-image: linear-gradient(to right, #000 50%, transparent);
}
三个必须注意的细节
- 必须写
color: transparent,否则文字颜色会盖住背景。有些浏览器还会要求同时写-webkit-background-clip与标准属性。 - 文字不能被选中时看不见:文字本体仍在,选中依然正常,只是颜色透明。可访问性不受影响(屏幕阅读器读的是文本内容)。
- 不要对超长正文使用:渐变文字在长段落中会显得廉价,而且大幅渐变层的渲染开销不低。标题、按钮、徽章是最合适的场景。
响应式:让形状随容器优雅缩放
形状最容易在响应式上翻车。原因很简单:形状的百分比坐标是相对于元素自身盒子的,元素变宽变窄,形状就会拉伸变形。下面是几种应对策略。
.hex-avatar {
width: clamp(80px, 20vw, 160px);
aspect-ratio: 1 / 1;
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
}
/* 策略二:用 CSS 变量把顶点参数化 */
.cut-corner {
--cut: clamp(12px, 4vw, 36px);
clip-path: polygon( 0 0, calc(100% - var(--cut)) 0, 100% var(--cut), 100% 100%, 0 100%);
}
/* 策略三:用容器查询单位 */
.card-wrap { container-type: inline-size; }
.card-shape {
clip-path: polygon( 0 0, calc(100% - 2cqw) 0, 100% 2cqw, 100% 100%, 0 100%);
}
/* 策略四:小屏退化成简单形状 */
.fancy {
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
}
@media (max-width: 600px) {
.fancy { clip-path: none; border-radius: 12px; }
}
一个容易被忽略的问题:阴影会被裁掉
给元素加了 clip-path 之后,box-shadow 也会被一起裁掉,因为阴影属于元素的绘制内容。想要形状带阴影,有两个办法:
.shape-1 {
clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
filter: drop-shadow(0 8px 16px rgba(37, 99, 235, 0.35));
}
/* 方案二:外层容器承载阴影,内层承载裁剪 */
.shape-wrap {
filter: drop-shadow(0 8px 20px rgba(15, 23, 42, 0.2));
}
.shape-inner {
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
}
/* 注意:filter 会创建新的包含块,可能影响 fixed 定位子元素 */
drop-shadow() 的性能开销比 box-shadow 高,因为它需要先计算出裁剪后的实际轮廓。所以只在需要阴影的元素上使用,不要大面积滥用。
性能、兼容性与调试
形状相关的属性对性能的影响差异很大,下面这张表可以作为选型参考。
| 属性 | 触发阶段 | 代价 | 建议 |
|---|---|---|---|
clip-path(静态) |
Composite | 低 | 放心使用,甚至可以大量使用 |
clip-path(动画中) |
Composite(多数情况) | 低 | 浏览器会走 GPU 路径,通常很流畅 |
border-radius(静态) |
Composite | 低 | 最优选择,还能保留边框与阴影 |
border-radius(动画中) |
Paint + Composite | 中 | 会重绘,别一屏放几十个 |
mask-image(渐变遮罩) |
Composite | 中 | 静态使用没问题,动态改变遮罩图较贵 |
shape-outside |
Layout | 高 | 静态排版用,绝对不要做动画 |
filter: drop-shadow() |
Paint + Composite | 高 | 只给少数关键元素使用 |
path() 裁剪 |
Composite | 中 | 顶点极多时解析开销上升 |
/* clip-path 基本图形 —— 全平台(IE 除外) */
/* border-radius 八值语法 —— 全平台 */
/* shape-outside —— Chrome 37+ / Safari 10.1+ */
/* mask(标准属性)—— Chrome 120+ 稳定 */
/* mask-composite —— Chrome 120+ / Safari 15.4+ */
/* background-clip: text —— 需要 -webkit- 前缀 */
/* @property —— Chrome 85+ / Safari 16.4+ */
/* aspect-ratio —— Chrome 88+ / Safari 15+ */
/* 用 @supports 做渐进增强 */
.card {
/* 基础:矩形圆角,谁都能看 */
border-radius: 12px;
}
@supports (clip-path: polygon(0 0, 100% 0, 100% 100%)) {
.card {
border-radius: 0;
clip-path: polygon( 0 0, calc(100% - 28px) 0, 100% 28px, 100% 100%, 0 100%);
}
}
/* JS 侧的特性检测 */
const supportsClip = CSS.supports('clip-path', 'polygon(0 0, 100% 0, 0 100%)');
const supportsMask = CSS.supports('mask-composite', 'intersect');
三条实践经验
- 优先用
border-radius:能做出来就别上clip-path,因为它保留边框、保留阴影、渲染路径最短。 - 动画只改
clip-path与transform:这两个走合成线程;shape-outside、filter一律不要动画。 - 顶点数量控制在一百以内:
polygon()的每个顶点都会被解析成一条边,几百个顶点的形状在低端设备上解析本身就会成为瓶颈。
无障碍:形状不能成为信息的唯一载体
形状很有表现力,但也容易带来可访问性问题。核心原则只有一条:形状是装饰,信息必须在文字或语义里同时存在。
aria-label 或语义化标签。形状只是增强,不是唯一通道。
clip-path 裁掉了一大块内容,导致关键文字不可见。裁剪区域内的内容对所有人都不存在。
padding,或者只裁背景层而不裁内容层。
.deco-shape {
aria-hidden: true; /* 写在 HTML 上 */
}
/* 状态形状 + 文字标签 */
<span class="badge badge--success">
<i class="fas fa-check" aria-hidden="true"></i>
已通过
</span>
/* 尊重减少动态偏好 */
@media (prefers-reduced-motion: reduce) {
.blob, .morph-shape, .text-clip-demo .line {
animation: none !important;
}
}
/* 高对比度模式下,纯装饰的形状应该让位 */
@media (forced-colors: active) {
.deco-shape { display: none; }
.shape-inner { clip-path: none; }
}
还有一点常被忽略:裁剪后的点击区域。这既是优点也是风险。优点是圆形按钮的四角不会误触;风险是如果用户看不清楚形状边界(比如低视力用户在高对比度模式下),可能不知道哪里可以点。解决办法是给形状元素保留一个足够大的点击热区,或者保证形状内部有清晰的文字。
创意形状最佳实践清单
把全文的结论浓缩成一份可以贴在工位上的清单。
- 先问“是不是真的需要形状”:能用一个圆角解决的问题,不要上
clip-path。 - 优先
border-radius:保留边框、保留阴影、渲染最省。 - 几何形状用
clip-path:三角形、六边形、斜切角、气泡尾巴。 - 渐变淡出用
mask:软边界是clip-path做不到的。 - 文字环绕用
shape-outside:但只在静态排版中使用,绝不做动画。 - 坐标优先用百分比:配合
aspect-ratio锁定比例,避免拉伸变形。 - 切角用
calc(100% - var(--cut)):让切角尺寸随视口自适应。 - 形状过渡要点数一致:两个 polygon 的顶点数量必须相同才能平滑插值。
- 需要插值的自定义属性用
@property声明类型。 - 阴影用
drop-shadow():box-shadow会被一起裁掉。 - 内容不要被裁掉:裁背景层,或者给内容留足内边距。
- 装饰性形状加
aria-hidden:别让屏幕阅读器读到无意义的元素。 - 尊重
prefers-reduced-motion:关掉形状动画与文字流光。 - 在低端真机上验证:
mask与filter在大面积使用时会明显吃 GPU。
/* 1. 形状变量与基础过渡 */
:root {
--shape-cut: 28px;
--shape-round: 14px;
--shape-ease: cubic-bezier(0.2, 0, 0, 1);
}
/* 2. 常用形状类 */
.shape-hexagon {
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
}
.shape-diamond {
clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
}
.shape-triangle {
clip-path: polygon(50% 0%, 100% 100%, 0% 100%);
}
.shape-arrow-right {
clip-path: polygon(0% 20%, 60% 20%, 60% 0%, 100% 50%, 60% 100%, 60% 80%, 0% 80%);
}
.shape-slant {
clip-path: polygon(15% 0%, 100% 0%, 85% 100%, 0% 100%);
}
.shape-cut-corner {
clip-path: polygon( 0 0, calc(100% - var(--shape-cut)) 0, 100% var(--shape-cut), 100% 100%, 0 100%);
}
.shape-blob {
border-radius: 63% 37% 54% 46% / 55% 48% 52% 45%;
}
.shape-leaf {
border-radius: 0% 100% 0% 100% / 0% 100% 0% 100%;
}
.shape-egg {
border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
}
/* 3. 形状动画基类(只动画 clip-path) */
.shape-anim {
transition: clip-path 0.5s var(--shape-ease);
will-change: clip-path;
}
.shape-anim.done { will-change: auto; }
/* 4. 带阴影的形状容器 */
.shape-shadow {
filter: drop-shadow(0 8px 18px rgba(15, 23, 42, 0.18));
}
/* 5. 无障碍降级 */
@media (prefers-reduced-motion: reduce) {
.shape-anim { transition: none; }
.blob, .morph-shape { animation: none; }
}
/* 6. 高对比度模式下放弃装饰形状 */
@media (forced-colors: active) {
.shape-hexagon, .shape-diamond, .shape-triangle,
.shape-arrow-right, .shape-slant, .shape-cut-corner {
clip-path: none;
border: 1px solid CanvasText;
}
}
最后:六边形网格示例
把上面所有技巧串起来,做一组带悬停反馈的六边形标签。它们全部由纯 CSS 绘制,可以随主题换色,悬停时有弹性缩放。
纯 CSS 六边形:clip-path + transition,零图片、零 SVG
.hex {
width: 92px;
height: 102px;
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
background: linear-gradient(135deg, #60a5fa, #2563eb);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 700;
transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), background 0.35s ease;
}
.hex:hover {
transform: scale(1.1) rotate(-3deg);
background: linear-gradient(135deg, #f59e0b, #ef4444);
}
形状这件事,说到底是把“设计稿里的轮廓”翻译成浏览器能理解的一组坐标。一旦你熟悉了 polygon() 的顶点逻辑、border-radius 的八值语法、mask 的明度语义和 shape-outside 的环绕规则,你就会发现:过去那些必须画在 Figma 里、导出成 SVG 的装饰元素,现在全部可以写进样式表,跟着主题变量一起呼吸。
更重要的是,纯 CSS 的形状是可响应的、可动画的、可访问的、可维护的。它不依赖任何外部资源,不会因为一次设计改版就需要重新导出几十张图片,也不会在暗色模式下突兀地亮着一块白色背景。下次当你想在页面里放一个六边形、一片叶子、一段弧形文字环绕时,先别打开设计工具——打开 DevTools,写一行 clip-path 试试。