CSS 中的响应式圆角

606 阅读5分钟

CSS 中的响应式圆角

不久前,当我在浏览 facebook 主页来学习他们是如何构建项目时,我很好奇那里的工程师们是怎么开发 CSS 的。我注意到 feed 流中的卡片组件的 border-radius 值的写法非常有意思。

我将这个小发现分享到推特 中。随后,我就收到了 Miriam Suzanne 的回复:

border-radius 的值总是 8px 吗?那样写看起来有点问题,((100vw - 4px) - 100%) 拿到的是个负数,将 9999 变为 -9999?那又是怎么将 border-radius 的值变成 0px 的呢?基本上,如果视口的宽度小于 4pxborder-radius 的值就变为 0px

几个小时后,来自 Facebook (Yay!) 的 Frank Yan 确认了这里存在一些条件判断,会在卡片充满整个视口时将 border-radius 的值从 8px 变为 0px

这不很神奇吗?

最初,我认为这是因为某些 bug 而引起的错误。还好,是我想错了。这篇文章中,我会针对这个问题去解释为什么要使用这种解决方案。

问题

我们有一个 border-radius8px 的卡片组件。这个卡片没有内外边距,或者说它的宽度充满整个视口,我们想将 border-radius 的值变为 0

我们可以通过设置 CSS 媒体查询去移除 border-radius 属性,如下所示。

@media (min-width: 700px) {
    .card {
        border-radius: 8px;
    }
}

在某些情况下,这样写是受限的。如果我们想当视口宽度小于 450px 时,重新设置 border-radius,我们就需要在创建一个 class 并且再次使用媒体查询。

@media (max-width: 450px) {
    .card--rounded {
        border-radius: 8px;
    }
}

解决方案

Facebook 团队曾提出一种骚气的解决方案。它的伪代码大致如下:

if (cardWidth >= viewportWidth) {
    radius = 0;
} else {
    radius = 8px;
}

如果想在 CSS 中实现这种逻辑,我们需要在 CSS 函数中去比较卡片宽度和视口宽度的大小。如果你不知道怎么做,我真心建议你看下这篇文章

这种解决方案的灵感来自 Heydon Pickering 的这篇文章(The Flexbox Holy Albatross)。后来被 Facebook 的 Naman Goel 改编并用在了 border-radius 上。

.card {
   border-radius: max(0px, min(8px, calc((100vw - 4px - 100%)
    * 9999)));
}

我们仔细看下上面这段 CSS 代码。

  1. 我们有一个 max() 方法去比较 0pxmin() 方法计算后的值。它将会选取两个值中较大的那个。
  2. min() 方法会比较 8pxcalc((100vw - 4px - 100%) * 9999)) 计算后的值。计算后的结果可能是一个很大的正数或很小的负数。
  3. 9999 是一个较大的常数来保证最后的计算结果一定是 0px8px

让我们来看下 calc() 这个方法的奇特之处。

奇特点在于 100% 这个值。对于两种不同的场景,结果会有所不同。

  • 它可以等于包含其元素(父元素)宽度的 100%
  • 当卡片占满整个视口宽度(比如:移动端)时,也可以等于 100vw

为什么常数选取 9999

并不是说 9999 这个常数有什么特别的魔力。只是为了考虑避免边界情况。这里感谢 Temani Afif 提醒了我这一点。

我们假设视口的宽度是 375px,其中容器宽度是 365px。如果我们把这些数值代入计算等式,计算结果如下。

.card {
    border-radius: max(0px, min(8px, calc(375px - 4px - 365px)));
    /* will result to */
    border-radius: max(0px, min(8px, 6px));
}

上面可以看到,6px 会被作为最后 border-radius 的值。然而这并不符合我们的预期。因为 border-radius 的值应该是 0px8px 的其中之一。为了解决这个问题,我们引入一个在 CSS 中不太可能用到的常数,如 9999

.card {
    border-radius: max(0px, min(8px, calc((375px - 4px - 365px)
     * 9999)));
    /* will result to */
    border-radius: max(0px, min(8px, 59994px));
}

这下,min() 方法计算后会得到 8px,再用 0px8px 求最大值,最后得到的仍是 8px

我们基于第一种场景举个例子。假设视口宽度是 1440px,卡片组件外部包裹着一个宽度为 700px 的父元素。

calc(1440px - 4px - 700px) 的结果乘以 9999 得到一个很大的数 7359264。这段 CSS 逻辑在浏览器端会变成如下:

.card {
    border-radius: max(0px, min(8px, 7359264px));
}

因为我们用了 min() 方法,它将会计算最小值结果是 8px。然后去使用 max() 方法时,计算后的结果的还是 8px。以上这段骚气的 CSS 在第一种场景下的处理。

.card {
    border-radius: 8px;
}

接下来,轮到第二种移动端的场景。注意移动端父元素的宽度和视口的宽度是相等的

calc(375px - 4px - 375px) 的结果乘以 9999 得到一个负数 -39996px。浏览器端将会变成:

.card {
    border-radius: max(0px, min(8px, -39996px));
}

现在有意思了!浏览器将面临两个问题:

  • 哪一个值更小呢?8px 还是 -39996px?答案是 -39996px
  • 哪一个值更大呢?0px 还是 -39996px?答案是 0px
.card {
    border-radius: 0px;
}

你看到了吗?到现在为止我还是被这种骚气的 CSS 写法所惊艳到。

Temani AfifLiad Yosef 还建议我们使用 CSS 的 clamp() 方法让代码看起来更加高大上。我觉得 Facebook 没有这么干是因为在老版本的 Safari 浏览器不支持(如:v12)。

.card {
    border-radius: clamp(0px, ((100vw - 4px) - 100%) * 9999, 8px);
}

在线运行

希望你喜欢这篇文章。感谢阅读!