Web 开发者对于 CSS 的 border-radius 属性并不会感到陌生,因为 Web UI 的圆角效果几乎都是使用 border-radius 来实现的。虽然 Web 开发者对于 border-radius 的基础运用是手到擒来,但对于 border-radius 属性中的一些细节,有可能是知其然不知其所以然。比如:
border-radius的百分比值(%)相对于谁计算?border-radius嵌套会发生什么?border-radius半径重叠会发生什么?border-radius遇到transform会发生什么?- 条件
border-radius又是如何实现(又称响应式圆角)?
我将在这节课中与大家一起探讨这些方面,希望能帮助大家更好理解和掌握 border-radius 属性中不为人知的一面。
border-radius 的基础使用
就 border-radius 的基础使用来说,它是很简单的,但我还是想花一点点时间来和大家一起重温下。先上张图:
上图就是 border-radius 属性最基础的运用,它和 padding 、margin 等属性是有点相似的,可以接受 1 ~ 4 个值,不同的是,border-radius 值代表的是元素框的顶角的圆角半径:
- 一个值 :矩形框四个顶角的圆角半径都相等;
- 两个值 :第一个值是矩形框左上角和右下角的圆角半径;第二个值是矩形框右上角和左下角的圆角半径;
- 三个值 :第一个值是矩形框左上角的圆角半径;第二个值是矩形框右上角和左下角的圆角半径;第三个值是矩形框右下角的圆角半径;
- 四个值 :第一个值是矩形框左上角的圆角半径;第二个值是矩形框右上角的圆角半径;第三个值是矩形框右下角的圆角半径;第四个值是矩形框左下角的圆角半径。
另外,border-radius 设置圆角半径时,还可以使用斜杆(/)分隔符设置 x 轴和 y 轴的半径,其中 / 前表示 x 轴方向半径,/ 后表示 y 轴方向半径。如果在 border-radius 中没有使用 / 分隔符,表示圆角的 x 轴和 y 轴半径相等。比如:
注意,/ 分隔符前后都可以是 1 ~ 4 个值,每个值所达的含义与不带 / 分隔是相同的,唯一差别就是 / 分隔符前是 x 轴方半径,分隔符后是 y 轴方向半径。
另外,在 border-radius 属性上要使用 / 分隔符时,建议你养成一个较好的习惯,在 / 分隔前后添加一个空格符。下图展示了 border-radius 取两个值时,有没有带分隔符的差异:
border-radius 也是一个简写属性,它可以拆分为四个属性:
border-top-left-radius:设置左上角圆角半径;border-top-right-radius:设置右上角圆角半径;border-bottom-right-radius:设置右下角圆角半径;border-bottom-left-radius:设置左下角圆角半径。
.box {
border-radius: 5rem 2rem 4rem 3rem;
/* 等同于 */
border-top-left-radius: 5rem;
border-top-right-radius: 2rem;
border-bottom-right-radius: 4rem;
border-bottom-left-radius: 3rem;
}如果你只想给元素框某一个顶点设置圆角,那么就可以使用 border-top-left-radius 、border-top-right-radius 、border-bottom-right-radius 或 border-bottom-left-radius 四个属性中的一个。比如:
.radius-top-left {
border-top-left-radius: 2rem;
/* 等同于 */
border-radius: 2rem 0 0 0;
}有一点需要注意的是,border-radius 的子属性有对应的逻辑属性:
但不幸的是,border-radius 并没有对应的逻辑属性。也就是说,在处理多语言 Web 应用或 Web 页面时,无法直接使用 border-radius (除非四个圆角半径相同)来给 UI 设置圆角,只能使用相应的子属性,只有这样才能让圆角匹配不同的书写模式和文本方向:
.element {
border-start-start-radius: 10px;
border-start-end-radius: 20px;
border-end-start-radius: 30px;
border-end-end-radius: 40px;
}CSS 的 border-radius 除了可以实现带有圆角的 UI 效果之外,还可以使用它来绘制一些图形,比如圆形、椭圆形等,甚至还可以使用它来构建一些带有艺术创意的 UI :
上面所介绍的是 CSS border-radius 最基础的知识以及其作用。接下来,我们来探讨一些大家不知道的方面。
border-radius 百分值相对于谁计算?
border-radius 值可以是:
- 固定值 :带有固定单位的长度值(
<length>),比如px单位值; - 相对值 :带有相对单位的长度值,比如百分比值(
%)、视窗单位值(vw、vh等)、容器查询单位(cqw、cqh等)、rem和em等; - 函数值 :
calc()、min()、max()和clamp()等函数表达式值。
相比而言,% 的取值是最为复杂的。特别是对于初学者而言,可能不太了解 border-radius 值为 % 时,它是相对于谁做计算。
border-radius 使用 % 值时,它的相对值是需要分开来计算的,其中 x 轴的 % 值相对于元素的 width 值计算;y 轴的 % 值相对于元素的 height 值计算,比如:
.element {
width: 300px;
height: 300px;
border-radius: 30% 70% 20% 40%;
}上面示例中 border-radius: 30% 70% 20% 40%; 对应的计算结果是:
x = width = 300px
y = height = 300px
a: 左上角(border-top-left-radius)
⇒ border-top-left-radius: 30%
⇒ a(x) = a(y) = 300px × 30% = 90px
b: 右上角(border-top-right-radius)
⇒ border-top-right-radius: 70%
⇒ b(x) = b(y) = 300px × 70% = 210px
c: 右下角(border-bottom-right-radius)
⇒ border-bottom-right-radius: 20%
⇒ c(x) = c(y) = 300px × 20% = 60px
d: 左下角(border-bottom-left-radius)
⇒ border-bottom-left-radius: 40%
⇒ d(x) = d(y) = 300px × 40% = 120px效果看上去像下图这样:
如果元素 width 和 height 不相等时:
.element {
width: 600px;
height: 300px;
border-radius: 30% 70% 20% 40%;
}这个时候,border-radius: 30% 70% 20% 40%; 对应的计算结果是:
x = width = 600px
y = height = 300px
a: 左上角(border-top-left-radius)
⇒ border-top-left-radius: 30%
⇒ a(x) = 600px × 30% = 180px
⇒ a(y) = 300px × 30% = 90px
b: 右上角(border-top-right-radius)
⇒ border-top-right-radius: 70%
⇒ b(x) = 600px × 70% = 420px
⇒ b(y) = 300px × 70% = 210px
c: 右下角(border-bottom-right-radius)
⇒ border-bottom-right: 20%
⇒ c(x) = 600px × 20% = 120px
⇒ c(y) = 300px × 20% = 60px
d: 左下角(border-bottom-left-radius)
⇒ border-bottom-left: 40%
⇒ d(x) = 600px × 40% = 240px
⇒ d(y) = 300px × 40% = 120px效果看上去像下图这样:
如果元素的 width 和 height 相等,但 border-radius 属性的值是一个带 / 分隔符号的八个值:
.element {
width: 300px;
height: 300px;
border-radius: 70% 30% 30% 70% / 60% 40% 60% 40%;
}对应的计算如下:
x = width = 300px
y = height = 300px
左上角(border-top-left-radius)
⇒ border-top-left-radius: 70% / 60%
⇒ a = x = 300px × 70% = 210px
⇒ e = y = 300px × 60% = 180px
右上角(border-top-right-radius)
⇒ border-top-right-radius: 30% / 40%
⇒ b = x = 300px × 30% = 90px
⇒ f = y = 300px × 40% = 120px
右下角(border-bottom-right-radius)
⇒ border-bottom-right-radius: 30% / 60%
⇒ c = x = 300px × 30% = 90px
⇒ g = y = 300px × 60% = 180px
左下角(border-bottom-left-radius)
⇒ border-bottom-left-radius: 70% / 40%
⇒ d = x = 300px × 70% = 210px
⇒ h = y = 300px × 40% = 120px对应的效果如下:
同样的,如果元素的 width 和 height 值不同时,计算方式相似:
.element {
width: 600px;
height: 300px;
border-radius: 70% 30% 30% 70% / 60% 40% 60% 40%;
}对应的计算如下:
x = width = 600px
y = height = 300px
左上角(border-top-left-radius)
⇒ border-top-left-radius: 70% / 60%
⇒ a = x = 600px × 70% = 420px
⇒ e = y = 300px × 60% = 180px
右上角(border-top-right-radius)
⇒ border-top-right-radius: 30% / 40%
⇒ b = x = 600px × 30% = 180px
⇒ f = y = 300px × 40% = 120px
右下角(border-bottom-right-radius)
⇒ border-bottom-right-radius: 30% / 60%
⇒ c = x = 600px × 30% = 180px
⇒ g = y = 300px × 60% = 180px
左下角(border-bottom-left-radius)
⇒ border-bottom-left-radius: 70% / 40%
⇒ d = x = 600px × 70% = 420px
⇒ h = y = 300px × 40% = 120px对应的效果如下图所示:
我想你已经知道了 border-radius 取百分比值,它是相对于谁进行计算了。在实际使用过程中,万一碰到圆角效果未达到自己预期,也知道如何修复:
border-radius 嵌套时会发生什么?
在 Web 开发过程中,在使用 border-radius 的时候,有时会产生圆角嵌套的视觉效果:
同一个元素上,发生圆角嵌套的场景一般会有:
- 带有边框的圆角场景;
- 带有内距的圆角场景;
- 同时带有边框和内距的圆角场景。
比如:
.box {
border-radius: 50px;
}
.box--border {
border: 30px solid #f90;
}
.box--padding {
padding: 30px;
}
.box--border--padding {
padding: 30px;
border: 30px solid #f36;
}正如上面示例所示,它们都产生了圆角嵌套的效果:
CSS 的 border-radius 定义了元素外圆角,以下几个场景将会发生内圆角:
- 当
border-radius的值大于border-width时,会产生内圆角,并且内圆角的半径为border-radius - border-width; - 当
border-radius的值大于padding时,也会产生内圆角,并且内圆角的半径为border-radius - padding。
上面这两种场景刚好对应的是上图中的左图和中间图。另外,要是元素同时出现 border-radius、border-width 和 padding ,将会产生多重内嵌套圆角,如上图最右侧效果。
设置 border-radius 的元素,会不会产生内圆角,取决于:
- 如果
border-radius的值大于border-width时,则会产生内圆角; - 如果
border-radius的值大于padding时,则会产生内圆角; - 如果
border-radius的值等于或小于border-width的值(或padding值),则不会产生内圆角。
也就是说,当内圆角产生时,内圆角的半径(r)则是它们之间的差,即 border-radius - border-width、border-radius - padding 或 border-radius - border-width - padding。比如上例:
r = border-radius - border-width = 50px - 30px = 20px
r = border-radius - padding = 50px - 30px = 20px
r = border-radius - border-width - padding = 50px - 30px - 30px = -10px你可以尝试着在下面的示例中调整 border-radius、padding 和 border-width 的值,查看元素圆角上的变化:
如果 border-width 和 padding 每个方向取值不同,这个时候和 border-radius 产生的差值也将会不一样,也会出现内圆角的 x 轴和 y 的半径不同,内圆角的效果也将会类似于 border-radius 设置了 x 和 y 轴的半径,比如 border-radius: 10px / 20px。我们在上面的示例稍作调整,效果如下:
尝试着调整示例中的相关参数,你可以看到相关变化。我将相应的圆角半径标注出来,从图中可以明白其中的变化:
从上图效果也不难发现,尽管设置 border-width 或 padding,以及同时设置这两个值,并且每个方向的值不同,如果 border-radius 和它们产生的差值大于 0, 就会产生内部圆角,而且圆角的半径就等于其差值。
前面,我们聊的是同一个元素上,border-radius 和 border-width 以及 padding 会让元素产生嵌套圆角。但是,Web 上有很多类似下图这种圆角嵌套的 UI 效果,一个圆角容器内嵌套一个圆角元素:
当两个元素的 border-radius 属性使用相同值时,圆角效果看上去会非常地奇怪:
<div class="box--wrapper">
<div class="box"></div>
</div>
.box--wrapper {
border-radius: 32px;
padding: 16px;
}
.box {
border-radius: 32px;
}你可能会感到困惑,两个元素的 border-radius 值都是 32px ,为什么两个元素的圆角没有匹配对呢?其实原因很简单,它们在值上是匹配的(都是 32px ),但它们在数学上是不匹配的 。因此,你所看到的嵌套圆角效果是不完美的,内部元素的圆角看起来笨拙或过大。
幸运的是,我们可以使用一个小公式来创建相对大小的完美圆角:
R = P + r或者:
r = R - P- R 是圆角容器的
border-radius; - P 是圆角容器与圆角元素之间的间距,比如圆角容器的内距
padding; - r 是圆角元素的
border-radius。
回到上面示例中,这三个参数分别对应着:
R对应着圆角容器.box--wrapper的border-radius;P对应着圆角容器.box--wrapper的padding;r对应着圆角元素.box的border-radius。
由于 R 和 r 是一对相对值,要么 R 相对于 r 计算(R = P + r),要么 r 相对于 R 计算(r = R - P)。一般情况下,首先会有 R 和 P ,所以我更建议选择 r 相对于 R 来计算。
不管是 R = P + r 还是 r = R - P ,它们只是一个数学表达式。我们可以使用 calc() 函数,将其运用于 CSS 中。另外,为了避免每次去计算,可以借助 CSS 的自定义属性让事情变得更简单一些:
.box--wrapper {
--R: 32px;
--P: 10px;
--r: calc(var(--R) - var(--P));
padding: var(--P);
border-radius: var(--R);
}
.box {
border-radius: var(--r);
}上面展示的只是圆角容器有一个内距 P ,但是圆角容器的边框粗细(border-width)同样对 border-radius 有影响。如果把边框粗细考虑进来,我们可以把公式扩展为:
r = R - B - P或者:
R = B + P + r其中:
R是容器自身的border-radius的半径(外圆角半径);B是容器自身的border-width的值(边框粗细);P是容器自身的padding的值(内距);r是内容区域的border-radius的半径(内圆角的半径)。
如果是多个元素嵌套,且只在最外的容器显式设置 border-radius 值,那么第一层嵌套的子元素的圆角半径将按上面的公式计算获得,计算出来的半径值将成为第二层的子元素的圆角半径(R)。依此类推,直到计算出来的 border-radius 的值为 0(小于 0 的值会被视为 0)。
我想说的是,我们在还原 UI 的时候,需要考虑内外部元素之间的圆角半径之间的关系,这样在视觉的还原上会更协调。
border-radius 重叠时会发生什么?
Web 中有一种 UI 风格,它看上去像“胶囊”的外形:
我们常把这种 UI 的风格称作“胶囊 UI ”,这种“胶囊 UI ”常用于一些 button、checkbox 和 radio 的元素上。
为了能一劳永逸,CSS 实现胶囊 UI 时,会给元素指定一个很大的 border-radius 值,比如 999rem、999vmax之类的。这样做不管元素高度是多少,都可以实现胶囊 UI 的效果:
.pill {
border-radius: 999vmax;
}这意味着我们不需要知道元素(矩形框)的尺寸,它也能正常的工作。不过,在某些边缘情况上,会遇到一些奇怪的行为。比如在上面的示例基础上稍作调整,就是把 border-radius 的值设置为:
.pill {
border-radius: 100px 999vmax 999vmax 100px;
}你会发现,.pill 的左上角和左下角并没有看到任何圆角效果,可代码中明明设置了 border-top-left-radius 和 border-bottom-left-radius 的值为 100px 。
为什么 border-top-left-radius: 100px 和 border-bottom-left-radius: 100px 就消失了?它们去哪了?
Let f = min(Li/Si), where i ∈ {top, right, bottom, left}, Si is the sum of the two corresponding radii of the corners on side i, and Ltop = Lbottom = the width of the box, and Lleft = Lright = the height of the box. If f < 1, then all corner radii are reduced by multiplying them by f.
具体的解释请看下图:
公式看上去令人感到困惑,甚至是令人头痛。但我们只需要记住一点:这个公式的目的是防止 border-radius (圆角半径)重叠。简单地说:
客户端(浏览器)本质上是在想:“按比例缩小所有半径(
border-radius),直到它们之间没有重叠”!
我们来用简单的示例来阐述上述公式的一些基本原理,这样可以让大家更好的理解。
首先,它会计算矩形(元素)每条边的长度与与它接触的半径之和的比值:
元素每条边宽度 ÷ (相邻圆角半径1 + 相邻圆角半径2)比如元素 .pill 设置的样式:
.pill {
width: 600px;
height: 200px;
border-radius: 400px;
}就该示例而言,按照上面提供的公式就可以“计算出 .pill 元素每条边的长度与与它接触的半径之和的比率”:
然后将所有圆角的半径去乘以这些比率值(每条边计算出来的比率值)中的最小值。上例中计算出来的比率值只有 .75 和 .25,取更小的值 .25,那么计算出来的圆角半径值则是:
400px x .25 = 100px元素 .pill 的 height 是 200px(最短的边长),计算出来的 border-radius 刚好是 height 的一半,即 100px。这也让我们实现了一个“胶囊” UI 效果。
为了能了解得更清楚一些,我们回到前面有问题的示例中,只不过我们用 400px 来替代 999vmax,比如:
.pill {
width: 600px;
height: 200px;
border-radius: 100px 400px 400px 100px;
}同样根据上面的公式来计算出每边的比例:
Ratio = 元素每条边宽度 ÷ (相邻圆角半径 1 + 相邻圆角半径 2)
Top » 600px ÷ (100px + 400px) = 1.2
Right » 200px ÷ (400px + 400px) = 0.25
Bottom » 600px ÷ (400px + 100px) = 1.2
Left » 200px ÷ (100px + 100px) = 1四个方向最小的比率是 0.25,那么所有指定圆角半径乘以这个比例:
Top-Left » 100px × 0.25 = 25px
Top-Right » 400px × 0.25 = 100px
Bottom-Right » 400px × 0.25 = 100px
Bottom-Left » 100px × 0.25 = 25px这样一来,运用于 .pill 元素的 border-radius 值为 25px 100px 100px 25px:
@Jay 在 Codepen 上提供了一个 Demo。这是一个更丰富的例子,展示了在不同情况下发生了什么。较大的尺寸是代码中指定的半径,较小的尺寸是浏览器如何协调它们(半径),防止半径的重叠:
所说,如果有一天你也碰到 border-radius 在视觉上被丢失了,那就不要再感到困惑了。这一切都是因为半径重叠所造成的,它是一种正常现象。知道这个原因之后,你也能很快的修复,实现所需要的圆角效果。
border-radius 遇到 transform 会发生什么?
有过 Web 动效开发经验的开发者,应该知道 CSS 中的 transform 会常用于动效开发中,但并不知道 transform 和 border-radius 一起使用的时候会发生什么?比如下面这个动效:
<div class="box"></div>
.box {
border-radius: 2rem;
transition: all .2s linear;
}
.box:hover {
transform: scaleX(1.5);
}这是一个简单的 Web 动效,使用 transform 来模拟元素 .box 的宽度或高度变化的动效。不难发现,指定 border-radius 的元素,在使用 transform 改变元素宽度或高度时,元素上的圆角并不会重新绘制,圆角仅随着 transform 被缩放了:
如果圆角要重新绘制就需要渲染引擎能重绘(Repaint),但 GPU 不会这样处理,它只处理像素,而不是元素的内容。因为 GPU 非常羞于处理像素(GPU 只需要处理呈现该元素的像素),所以 transform 操作的速度非常快。这也是使用 transform 来处理元素宽度或高度动效的主要原因之一。
如果要避免这种现象出现,就需要采用一些技术手段来规避。比如 @Rik Schennink 在他的文章《Animating CSS Width and Height Without the Squish Effect》中提到的九宫法(9-slice scaling)。我对 @Rik Schennink 的教程中的示例做了一些调整:
<div class="radius">
<div class="content"></div>
</div>.radius {
height: 100px;
display: flex;
justify-content: flex-start;
}
.radius::before,
.radius::after {
content: "";
width: 20px;
background: #098fae;
}
.radius::before {
border-radius: 20px 0 0 20px;
}
.content {
background: #098fae;
width: 1px;
transform: scale3d(1, 1, 1);
transform-origin: left;
}
.radius::after {
border-radius: 0 20px 20px 0;
transform: translate3d(0, 0, 0);
}
/* CSS Animation */
@keyframes right-animate {
0% {
transform: translate3d(0, 0, 0);
}
100% {
transform: translate3d(600px, 0, 0);
}
}
@keyframes move {
0% {
transform: translate3d(0, 0, 0);
}
100% {
transform: translate3d(-300px, 0, 0);
}
}
@keyframes center-animate {
0% {
transform: scale3d(1, 1, 1);
}
100% {
transform: scale3d(601, 1, 1);
}
}
.content {
animation: center-animate 10s linear infinite alternate;
}
.radius::after {
animation: right-animate 10s linear infinite alternate;
}
.radius {
animation: move 10s linear infinite alternate;
}虽然示例中展示的技术可以使圆角不会因为 transform 变形,但该技术方案相对而言比较繁琐,适应性较弱。尤其是那种不是纯色背景环境下,比如 .radius 的内容是一张图片,上面的示例的技术方案就无法满足需求了。
庆幸的是,还有一种更好的方案,使用 CSS Houdini 中的自定义属性 @property (也称 CSS Houdini 中的变量),@property 可以进一步扩展 CSS 的动效。比如上面示例,采用 @property 可以像下面这样来实现:
@property --width {
initial-value: 1px;
inherits: false;
syntax: "<length>";
}
@keyframes square {
to {
--width: 300px;
}
}
.content {
width: var(--width);
animation: square 2s ease infinite alternate;
}上面展示的只是示例用到的关键代码(@property 和 @keyframes 部分),详细代码请查阅 Codepen上的示例。示例中左侧采用的是 CSS Flexbox 布局,右侧采用的是 CSS Grid 布局。但动效采用的是相同的方案,效果几乎是一样的:
条件圆角(响应式圆角)
在一些设计方案中,有些元素的圆角半径(border-radius)很大,但希望在移动端上更小一些。比如下图这个设计:
桌面端(宽屏)中卡片的圆角 border-radius 是 8px,移动端(窄屏)是 0。以往你可能是这样来写:
.card {
border-radius: 0;
}
@media only screen and (min-width: 700px) {
.card {
border-radius: 8px;
}
}未来你还可以使用 CSS 容器查询,像下面这样编写代码:
.card--container {
container-type: inline-size;
}
.card {
border-radius: 0;
}
@container (width > 700px) {
.card {
border-radius: 8px;
}
}其实,除了使用 CSS 查询特性之外,CSS 中还有一些其他的方式来实现上图的效果。简单地说,根据上下文环境来改变属性的值 。
比如,使用 CSS 的 clamp() 函数,就是一个不错的选择:
:root {
--w: 760px;
--max-radius: 8px;
--min-radius: 0px; /* 这里的单位不能省略 */
--radius: (100vw - var(--w));
--responsive-radius: clamp(
var(--min-radius),
var(--radius) 1000,
var(--max-radius) );
}
div {
border-radius: var(--responsive-radius, 0);
}你也可以将 min() 和 max() 组合起来一起使用,达到 clamp() 相似的功能,即 clamp(MIN, VAL, MAX) 等同于 max(MIN, min(VAL, MAX)) 。
注意,
min()、max()函数中可以直接进行四则运算,不需要使用calc()函数。
.box {
--min-radius: 0px;
--max-radius: 8px;
--ideal-radius: 4px;
border-radius: max(
var(--min-radius),
min(
var(--max-radius),
(100vw - var(--ideal-radius) - 100%) * 9999
)
);
}在未来,还可以使用 CSS 的 @when 和 @if 规则来实现条件圆角:
@when container(width >= 100vw) {
.box {
border-radius: 0;
}
}
@else {
.box{
border-radius: 1em;
}
}关于 @when 和 @if 没有太多要说的,因为到目前为止,还没有浏览器实现它,而且可能需要一段时间才能使用它。相对而言,容器查询 @container是更符合乎逻辑的。
小结
CSS 的 border-radius 已经存在很多年了,我想很多同学在平时的开发中都会使用它来实现一些圆角效果,而且还会使用它制作一些有趣的 UI 效果。但我想很多同学在使用border-radius会碰到一些怪异的现象,比如课程中所提到的圆角的嵌套,圆角重叠时按比例缩小等。
通过这节课的学习,你能解答平时使用 border-radius 时碰到的一些怪异的现象,即 使用border-radius怪异的现象!