创意形状与裁剪路径

张玥 2026年9月18日 阅读时间 40分钟
CSS形状 clip-path 遮罩 Mask 创意视觉 图形设计
CSS高级技巧视觉效果之创意形状与裁剪路径

网页的默认形状只有一个:矩形。而人的眼睛天生对矩形缺乏兴趣。过去我们要在页面里做出一个六边形、一个水滴、一个环形、一段让文字绕着走的曲线,只能导出 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 裁掉形状之外的部分,最常用、最直接
mask 用另一张“图”决定哪些像素可见,能做渐变淡出
border-radius 八值语法可以造出叶片、水滴、花瓣等有机形状
shape-outside 改变文字环绕的轮廓,而不是改变元素本身的视觉

一个关键的认知转变

很多开发者第一次接触 clip-path 时会问:“被裁掉的部分还能被点击吗?”答案是:不能。这是一个巨大的优点。用 clip-path 裁出来的圆形按钮,只有圆形区域可点击,四个角是彻底“透明”的。用图片做同样的事情,你必须写额外的 JS 去判断点击坐标——而 CSS 免费帮你做了。

clip-path 快速上手:四种基本图形

clip-path 接受一个“基本图形函数”作为值。最常用的有五个:inset()、circle()、ellipse()、polygon()、path()。前四个都支持百分比,会随元素尺寸自动缩放;path() 使用的是 SVG 路径语法,坐标是绝对数值,响应式能力最弱,除非你配合 viewBox 思路去做换算。

/* ① inset() —— 内缩矩形,可带圆角 */
.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 技巧锁定宽高比。
/* 锁定 1:1,让形状在任何宽度下都不变形 */
.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() 可用 calc(100% - 24px) 能精确切角
顶点数一致 两个 polygon 要能过渡,点数必须相同
性能提示 顶点越多,GPU 裁剪的三角形越多,但影响通常很小

当形状“切掉”了内容怎么办

这是一个高频问题:给卡片加了斜切角之后,右上角的关闭按钮不见了。原因是被裁掉的部分不只是不可见,而是完全不存在——它不渲染、不响应事件、不参与可访问性树的可视范围。

解决办法是:把内容放进一个内层容器,给内层容器留出足够的 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 的百分比,让形状像液体一样流动。

@keyframes blobBreath {
  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 改变的是周围文字环绕它的方式。后者常被忽略,但它是做出“杂志感排版”的关键。

基本规则只有三条:

  1. 元素必须是浮动元素(float: left/right),否则 shape-outside 不生效。
  2. 元素必须有宽度和高度,否则没有可供环绕的轮廓。
  3. 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-image 遮罩图,黑色 = 可见,透明 = 不可见
mask-size / position / repeat 与 background 的对应属性语义一致
mask-composite add / subtract / intersect / exclude
mask-mode 选择按亮度还是按 alpha 通道计算

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 做出“质感标题”最直接的方式。

创意形状
CUT · MASK · SHAPE
/* 渐变文字(本文标题正在使用的写法) */
.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; }
}
aspect-ratio 锁定比例,形状不变形
CSS 变量 让切角、半径随视口自适应
容器查询 按组件自身宽度而非视口计算
小屏降级 复杂形状在窄屏反而降低可读性

一个容易被忽略的问题:阴影会被裁掉

给元素加了 clip-path 之后,box-shadow 也会被一起裁掉,因为阴影属于元素的绘制内容。想要形状带阴影,有两个办法:

/* 方案一:用 drop-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');
Layers 面板 检查裁剪是否意外创建了合成层
Paint flashing 看 border-radius / mask 动画的重绘范围
Rendering → Clip-path 部分版本可高亮裁剪路径边界
真机测试 低端安卓机对 mask / filter 尤其敏感

三条实践经验

  • 优先用 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
形状
裁剪
遮罩
环绕
渐变

纯 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 试试。