Web编程技术营地
研究、演示、创新

CSS Polygons

撰写时间:2025-12-27

修订时间:2026-01-01

矩形与正方形

body { * { display: inline-block; margin: 1em; } .rectangle { width: 100px; aspect-ratio: 16 / 9; background-color: lightskyblue; } .square { width: 100px; aspect-ratio: 1 / 1; background-color: thistle; } }

椭圆与圆形

body { * { display: inline-block; margin: 1em; } .ellipse { width: 100px; aspect-ratio: 1.5 / 1; border-radius: 50%; background-color: #9B59B6; } .circle { width: 100px; aspect-ratio: 1 / 1; border-radius: 50%; background-color: #E74C3C; } .rotated { width: 100px; aspect-ratio: 1.5 / 1; border-radius: 50%; background-color: #FFC0CBAA; transform: rotate(45deg); } }

在绘制椭圆与圆形时,其border-radius的值须设为50%,才能出现圆滑的圆角。而通过设置aspect-ratio,可转换为各种椭圆。使用rotate函数来旋转椭圆。

三角形

div本身为矩形,但通过设置其border属性值,可将其转换为一个三角形。

向上的三角形及其变体

一步一步来。先为一个边长为50px的正方形设置宽度值为10px的边框。

body { * { display: inline-block; margin: 1em; } > div { width: 50px; height: 50px; border: 10px solid gray; } }

还是正方形的图形,仍未获得任何灵感。

第二步,将各边框设为不同的颜色。

body { * { display: inline-block; margin: 1em; } > div { width: 50px; height: 50px; border-top: 10px solid red; border-left: 10px solid green; border-right: 10px solid blue; border-bottom: 10px solid mediumaquamarine; } }

边框与边框的交界处,各自占了一半的面积,从而出现了一条斜边。这些斜边将成为构建三角形的基本要素。

各种颜色的边框均为梯形,而非三角形。取青色的底边框为例,其高度不够,因此不能构成三角形。

小分支

当走到这一步,若将widthheight值均设为0,并且等量加大各边框的厚度,则会得到另一个精美的图形。

body { * { display: inline-block; margin: 1em; } > div { width: 0px; height: 0px; border-top: 50px solid red; border-left: 50px solid green; border-right: 50px solid blue; border-bottom: 50px solid mediumaquamarine; } }

这个图形以后可能会用得上。但在这里,还是回到原来的思路,继续往下走。

第三步,加大底边框的厚度值。

body { * { display: inline-block; margin: 1em; } > div { width: 50px; height: 50px; border-top: 10px solid red; border-left: 10px solid green; border-right: 10px solid blue; border-bottom: 50px solid mediumaquamarine; } }

有点像三角形了。但梯形的顶边应从一条线段缩减为一个点。观察图形,梯形顶边的长度,正好就是divwidth属性值。

故此,第四步,将divwidth属性值设为0

body { * { display: inline-block; margin: 1em; } > div { width: 0; height: 50px; border-top: 10px solid red; border-left: 10px solid green; border-right: 10px solid blue; border-bottom: 50px solid mediumaquamarine; } }

下面的青色三角形已长成,但由于divheight属性值仍为50px,导致左边框的绿色及右边框的蓝色被撑高了。

第五步,将divheight属性值设为0

body { * { display: inline-block; margin: 1em; } > div { width: 0; height: 0; border-top: 10px solid red; border-left: 10px solid green; border-right: 10px solid blue; border-bottom: 50px solid mediumaquamarine; } }

红色的上边框占用了空间,故第六步,将设置border-top的代码删除。

body { * { display: inline-block; margin: 1em; } > div { width: 0; height: 0; border-left: 10px solid green; border-right: 10px solid blue; border-bottom: 50px solid mediumaquamarine; } }

此时,绿色的左边框与蓝色的右边框仍显示出来。观察青色的三角色的数据。divborder-bottom决定了三角形的高度,而三角形底边的宽度为左边框与右边框的宽度之和。因此,不能删除设置border-leftborder-right的代码。

故此,第七步,将border-leftborder-right的颜色值设置为transparent,让它们变成完全透明。并依视觉适当调整三角形底边的宽度及其高度。

body { * { display: inline-block; margin: 1em; } > div { width: 0; height: 0; border-left: 50px solid transparent; border-right: 50px solid transparent; border-bottom: 100px solid mediumaquamarine; } }

一个非常完美的等腰三角形出来了。

要绘制不等腰的三角形?只需将左边框与右边框的值设置为不一样的值即可。

body { * { display: inline-block; margin: 1em; } > div { width: 0; height: 0; border-left: 30px solid transparent; border-right: 95px solid transparent; border-bottom: 100px solid mediumaquamarine; } }

三角形不要放得那么正?旋转它。

body { * { display: inline-block; margin: 1em; } > div { width: 0; height: 0; border-left: 30px solid transparent; border-right: 95px solid transparent; border-bottom: 100px solid mediumaquamarine; transform: rotate(45deg); } }

向下的三角形

左、右边框为透明,保留上边框,不要下边框。

body { * { display: inline-block; margin: 1em; } > div { width: 0; height: 0; border-top: 100px solid steelblue; border-left: 50px solid transparent; border-right: 50px solid transparent; } }

向右的三角形

上、下边框为透明,保留左边框,不要右边框。

body { * { display: inline-block; margin: 1em; } > div { width: 0; height: 0; border-top: 50px solid transparent; border-bottom: 50px solid transparent; border-left: 100px solid steelblue; } }

箭头

得到向右的三角形后,通过使用::before, ::after这两个伪类及绝对定位的机制,可扩展绘制出一个漂亮的箭头。

body { padding: 2em; div.arrow { --arrow-body-width: 150px; --arrow-body-height: 15px; --arrow-head-width: 45px; --arrow-head-height: 50px; --arrow-color: skyblue; outline: 1px solid gray; width: calc(var(--arrow-body-width) + var(--arrow-head-width)); height: var(--arrow-head-height); position: relative; &::before { content: ''; width: var(--arrow-body-width); height: var(--arrow-body-height); background-color: var(--arrow-color); border-top-left-radius: 0.3em; border-bottom-left-radius: 0.3em; position: absolute; top: calc(var(--arrow-head-height) / 2 - var(--arrow-body-height) / 2); left: 0; } &::after { content: ''; width: 0; height: 0; border-top: calc(var(--arrow-head-height) / 2) solid transparent; border-bottom: calc(var(--arrow-head-height) / 2) solid transparent; border-left: var(--arrow-head-width) solid var(--arrow-color); position: absolute; top: 0; right: 0; } } }

最外层通过outline绘制了一个不占用盒模型空间的灰色边框,以检查各个组件是否精准到位。将此边框擦除,即是一个干净、完整的箭头。

箭头的形状与颜色使用CSS变量来定义,只须改变5个变量值,即可得到形状各异的箭头。

应用clip-path来绘制

使用CSS Masking Module中的clip-path,我们就可以像Canvas, SVG一样,在CSS中通过调用相关的CSS函数直接绘制CSS Shapes Module所定义的基本几何图形了。并且,CSS函数提供了更为精细的控制。

circle

body { --box-side: 80px; --hue-value: 100; div#container { padding: 1em; display: grid; grid-template-columns: repeat(auto-fit, var(--box-side)); justify-content: center; gap: 3em 0.5em; > div { aspect-ratio: 1 / 1; background-color: hsl(var(--hue-value), 50%, 50%); &:nth-child(1) { clip-path: circle(calc(var(--box-side)/2) at center); } &:nth-child(2) { clip-path: circle(calc(var(--box-side)/2) at left); } &:nth-child(3) { clip-path: circle(calc(var(--box-side)/2) at right); } &:nth-child(4) { clip-path: circle(calc(var(--box-side)/2) at top); } &:nth-child(5) { clip-path: circle(calc(var(--box-side)/2) at bottom); } &:nth-child(6) { clip-path: circle(calc(var(--box-side)/2) at top left); } &:nth-child(7) { clip-path: circle(calc(var(--box-side)/2) at top right); } &:nth-child(8) { clip-path: circle(calc(var(--box-side)/2) at bottom left); } &:nth-child(9) { clip-path: circle(calc(var(--box-side)/2) at bottom right); } &:nth-child(2), &:nth-child(3), &:nth-child(4), &:nth-child(5) { grid-row: 2; background-color: hsl(calc(var(--hue-value) + 120), 50%, 50%); } &:nth-child(6), &:nth-child(7), &:nth-child(8), &:nth-child(9) { grid-row: 3; background-color: hsl(calc(var(--hue-value) + 240), 50%, 50%); } } } }

第一行绘制整个圆。第二行绘制半圆。第三行绘制四分之一的圆。

circle的第一个参数指定半径,at之后的第二个参数指定在div的哪个位置建立起圆心。

ellipse

body { --box-side: 80px; --radius-length: calc(var(--box-side) / 2); --hue-value: 150; div#container { padding: 1em; display: grid; grid-template-columns: repeat(auto-fit, var(--box-side)); justify-content: center; gap: 3em 2em; > div { aspect-ratio: 1 / 1; background-color: hsl(var(--hue-value), 50%, 50%); &:nth-child(1) { clip-path: ellipse(var(--radius-length) var(--radius-length) at center); } &:nth-child(2) { background-color: hsl(calc(var(--hue-value) + 120), 50%, 50%); clip-path: ellipse(calc(var(--radius-length) + 10px) var(--radius-length) at center); } &:nth-child(3) { background-color: hsl(calc(var(--hue-value) + 120), 50%, 50%); clip-path: ellipse(calc(var(--radius-length) + 10px) calc(var(--radius-length) - 10px) at center); } &:nth-child(4) { background-color: hsl(calc(var(--hue-value) + 240), 50%, 50%); clip-path: ellipse(50px 50px at center); } } } }

椭圆有长半径与短半径之分。当长半径等于短半径时,成为一个正圆。如第一个图形所示。

原则上可以自由取椭圆的长半径与短半径的值,只要它们大于0就行。但在这里,需注意的是我们是在一个div的裁剪路径中绘图,因此其图形受div的大小影响,超出div范围的绘制部分将被裁剪。

上面每个图形都是在div的中心建立起圆心,因此--radius-length正好等于边长的一半。第一个图形的长短半径均取值于--radius-length,即40px,故所绘制出来的图形完全在div的范围内,未产生裁剪,因此它得以全部显示出来为一个正圆。

第二个图形,X轴半径为50px,超出了正圆半径40px,而Y轴半径依旧正好为40px,则在水平方向上超出div区域的部分将被裁剪,故其左右两边露出了div的左、右边框。故而形成了一个水桶的形状。

第三个图形,在第二个图形的基础上,适当降低Y轴半径,则水桶的比例特征更明显。

第四个图形,X轴及Y半径均超出正圆半径一些,但又不能太多,以让圆弧落在div范围内,则四角出现了近乎直线的圆弧。

inset

inset函数用于指定缩进范围,以及指定圆角。

body { --box-side: 80px; --radius-length: calc(var(--box-side) / 2); --hue-value: 150; div#container { padding: 1em; display: grid; grid-template-columns: repeat(auto-fit, var(--box-side)); justify-content: center; gap: 3em 2em; > div { aspect-ratio: 1 / 1; background-color: hsl(var(--hue-value), 50%, 50%); &:nth-child(1) { clip-path: inset(0px); } &:nth-child(2) { clip-path: inset(20px 10px); } &:nth-child(3) { clip-path: inset(10px 20px); } &:nth-child(4) { clip-path: inset(10px 20px 30px 40px); } &:nth-child(5) { background-color: hsl(calc(var(--hue-value) + 120), 50%, 50%); grid-row: 2; clip-path: inset(0px round 50%); } &:nth-child(6) { background-color: hsl(calc(var(--hue-value) + 120), 50%, 50%); grid-row: 2; clip-path: inset(0px round 30% 20%); } &:nth-child(7) { background-color: hsl(calc(var(--hue-value) + 120), 50%, 50%); grid-row: 2; clip-path: inset(0px round 80% 20%); } &:nth-child(8) { background-color: hsl(calc(var(--hue-value) + 120), 50%, 50%); grid-row: 2; clip-path: inset(0px round 10% 20% 30% 40%); } &:nth-child(9) { background-color: hsl(calc(var(--hue-value) + 240), 50%, 50%); grid-row: 3; clip-path: inset(0px round 50% / 10% 20% 30% 40%); } } } }

第一行仅缩进。该行上的第一个图形的参数为0px,则没有任何缩进;第二个及第三个分别在垂直及水平方向上缩进了不同的数值。第四个图形的参数为4个数值,分别对应上、右、下、左的缩进量。

第二行及第三行仅指定圆角。该行上的第一个图形使用round,指定了4个圆角均为50%,则为正圆。第二个图形的round2个参数,分别对应左上、右下以及右上、左下的值。第三个图形参数个数一样,但第1个参数值较大,则出现了椭圆的效果。第四个图形使用了4个参数,分别指定了左上、右上、右下、左下的圆角。

每个圆角均以椭圆的方式来定义,因此均可分别指定每个圆角在X轴上的半径及在Y轴上的半径。第三行的图形,先将4个圆角在X轴上的半径统一定义为50%,然后以/符号为分隔符,将4个圆角在Y轴上的半径分别按左上、右上、右下、左下的顺序予以指定。

xyhw

xyhw指定一个具有偏移坐标值及宽度、长度值的矩形。

body { --box-side: 100px; --radius-length: calc(var(--box-side) / 2); --hue-value: 170; div#container { padding: 1em; display: grid; grid-template-columns: repeat(auto-fit, var(--box-side)); justify-content: center; gap: 3em 2em; > div { aspect-ratio: 1 / 1; background-color: hsl(var(--hue-value), 50%, 50%); &:nth-child(1) { grid-row: 1; grid-column: 1; border: 1px dashed gray; background-color: rgb(66 66 66 / 0.5); } &:nth-child(2) { grid-row: 1; grid-column: 1; background-color: hsl(calc(var(--hue-value)), 50%, 50%); clip-path: xywh( calc(var(--box-side)/4) calc(var(--box-side)/4) 50% 50% round 0.5em ); } } } }

rect

rect以为各边框分别指定缩进的偏移值来指定一个矩形。

body { --box-side: 100px; --radius-length: calc(var(--box-side) / 2); --hue-value: 170; div#container { padding: 1em; display: grid; grid-template-columns: repeat(auto-fit, var(--box-side)); justify-content: center; gap: 3em 2em; > div { aspect-ratio: 1 / 1; background-color: hsl(var(--hue-value), 50%, 50%); &:nth-child(1) { grid-row: 1; grid-column: 1; border: 1px dashed gray; background-color: rgb(66 66 66 / 0.5); } &:nth-child(2) { grid-row: 1; grid-column: 1; background-color: hsl(calc(var(--hue-value)), 50%, 50%); clip-path: rect(0% 100% 100% 0%); } &:nth-child(3) { grid-row: 1; grid-column: 2; border: 1px dashed gray; background-color: rgb(66 66 66 / 0.5); } &:nth-child(4) { grid-row: 1; grid-column: 2; background-color: hsl(calc(var(--hue-value) + 180), 50%, 50%); clip-path: rect(10% 80% 70% 20%); } } } }

rect函数有4个参数,分别对应于顶边框、右边框、底边框、左边框的缩进偏移值。

但参考边框只有顶边框左边框两个。这意味着第1个参数及第3个参数是距离顶边框的偏移值,第2个参数及第4个参数是距离左边框的偏移值。

青色图形的第1个参数为0%,代表其顶边框的偏移值,即离顶边框的距离为0%,因此正好与原来的顶边框齐平;第3个参数为100%,代表其底边框的偏移值,即离顶边框的距离为100%,因此正好与原来的底边框齐平。

同样,青色图形的第2个参数为100%,代表其右边框的偏移值,即离左边框的距离为100%,因此正好与原来的右边框齐平;第4个参数为0%,代表其左边框的偏移值,即离左边框的距离为0%,因此正好与原来的左边框齐平。

因此,青色图形正好将原来的图形完全盖住了。

而红色图形的4个参数,分别意味着,顶边框比原来的顶边框偏移10%,右边框比原来的左边框偏移80%,下边框比原来的顶边框偏移70%,左边框比原来的左边框偏移20%

polygon

polygon根据所提供的各个点,绘制一个封闭的多边形。

body { --box-side: 80px; --radius-length: calc(var(--box-side) / 2); --hue-value: 150; div#container { padding: 1em; display: grid; grid-template-columns: repeat(auto-fit, var(--box-side)); justify-content: center; gap: 3em 2em; > div { aspect-ratio: 1 / 1; background-color: hsl(var(--hue-value), 50%, 50%); &:nth-child(1) { grid-row: 1; grid-column: 1; border: 1px dashed gray; background-color: rgb(66 66 66 / 0.5); } &:nth-child(2) { grid-row: 1; grid-column: 1; background-color: hsl(calc(var(--hue-value)), 50%, 50%); clip-path: polygon(nonzero, 0 0, 100% 0, 50% 80px); } &:nth-child(3) { grid-row: 1; grid-column: 2; border: 1px dashed gray; background-color: rgb(66 66 66 / 0.5); } &:nth-child(4) { grid-row: 1; grid-column: 2; background-color: hsl(calc(var(--hue-value)), 50%, 50%); clip-path: polygon(50% 0, 0 50%, 50% 100%, 100% 50%); } } } }

polygon的第1个参数可选,用于指定填充规则 (filling rule),默认值为nonzero,另一可选值为evenodd,具体详见Canvas Fill Rule

2个参数是由多个(x, y)坐标值所组成的一组数值。每组坐标值之间、各组坐标值之间,可用空格或,号隔开。

坐标值是相对于div左上角的偏移值,当为非零值时,数值后面须带有明确的CSS单位,如10px, 50%, 3em等等。

一般使用polygon来绘制由直线组成的多边形,若需要绘制弧线,应使用path

尽管CSS Shapes Module Level 1规范了polygon有一可选参数round,但SafariChrome目前尚未支持。

path

path绘制曼妙多姿的各种弧线。

body { --box-side: 300px; --radius-length: calc(var(--box-side) / 2); --hue-value: 90; div#container { padding: 1em; display: grid; grid-template-columns: repeat(auto-fit, var(--box-side)); justify-content: center; gap: 3em 2em; > div { aspect-ratio: 1 / 1; background-color: hsl(var(--hue-value), 50%, 50%); &:nth-child(1) { grid-row: 1; grid-column: 1; border: 1px dashed gray; background-color: rgb(66 66 66 / 0.5); } &:nth-child(2) { grid-row: 1; grid-column: 1; background-color: hsl(calc(var(--hue-value)), 50%, 50%); clip-path: path(nonzero, "M 134,25 C 9,152 240,184 205,233 C 38,141 280,97 60,229 C 61,130 100,86 152,98 S 202,52 165,45 Z"); transform: translate(20px, 15px); } } } }

path的第1个参数可选,用于指定填充规则 (filling rule),默认值为nonzero,另一可选值为evenodd,具体详见Canvas Fill Rule

2个参数是一个字符串,使用SVGpath命令格式。字符串的末尾如果没有关闭图形的z命令时,浏览器将自动添加,以自动关闭图形。

shape

path函数存在着一些先天不足。

一是命令及参数都以字符串的形式存在,因此我们需自己构建此字符串(本站的SVG Path Editor提供了相应的功能)。故此,它不能直接使用CSS变量。

二是默认只能使用px作为单位,因此在构建自动感应 (auto responsive) 的图形时,无法使用%的单位来保持图形的整体比例。

为解决这些问题,shape函数应运而生。它自带一套与SVGpath标签的d属性相对应的子命令;支持CSS变量;支持各种各样的CSS单位,绘制贝塞尔曲线时统一归置控制点,等诸多优势。

body { --box-side: 200px; --radius-length: calc(var(--box-side) / 2); --hue-value: 90; div#container { padding: 1em; display: grid; grid-template-columns: repeat(auto-fit, var(--box-side)); justify-content: center; gap: 3em 2em; > div { aspect-ratio: 1 / 1; background-color: hsl(var(--hue-value), 50%, 50%); &:nth-child(1) { grid-row: 1; grid-column: 1; border: 1px dashed gray; background-color: rgb(66 66 66 / 0.5); } &:nth-child(2) { grid-row: 1; grid-column: 1; background-color: hsl(calc(var(--hue-value)), 50%, 50%); clip-path: shape( from 5% 0%, hline to 95%, curve to right 5% with right top, vline to 92%, curve to 95% 97% with right 97%, hline to 70%, line by -2% 3%, line by -2% -3%, hline to 5%, curve to left 92% with left 97%, vline to 5%, curve to 5% top with left top ); } } } }

上面的shape函数,较多使用了%作为CSS单位,则在我们通过CSS变量--box-side修改div的尺寸大小时,所绘制的图形自动按比例缩放。

from子命令用于确定当前点的位置。

curve to子命令使用了right, top等关键字来表示位置,非常直观;with后面则紧随控制点的信息,整洁有序。

使用to来表示绝对定位,使用by来表示相对定位。

结语

clip-path让我们得以在特定标签的范围内绘制图形,则该标签立即转换为一个图形。而各种各样丰富的绘图函数,让CSS的绘图能力丝毫不逊于canvasSVG。因此,在纯CSS环境中,我们得到了原生的图像资源。

有了这些丰富的资源,再进一步应用到CSS动画,或直接作为CSS背景图像,将使HTML网页的渲染机制更加丰富、灵活而多样。

参考资源

  1. CSS Backgrounds and Borders Module Level 3
  2. CSS Shapes Module Level 1: Basic Shapes