我最近遇到了一个有趣的问题。我需要实现一个卡片网格,这些卡片具有可变(用户设置)的纵横比,该纵横比存储在自定义属性 --ratio
中。具有特定纵横比的盒子是 CSS 中的一个经典问题,近年来解决起来变得更容易,尤其是在我们获得 aspect-ratio
之后,但这里棘手的部分是每个卡片都需要在相对的角上有两个锥形渐变,并在对角线上交汇。如下所示

这里的挑战在于,虽然使用例如 to top left
这样的方向(随纵横比变化)沿可变纵横比盒子的对角线进行突然更改很容易,但 conic-gradient()
需要一个角度或一个百分比来表示它围绕完整圆圈旋转了多远。
查看 本指南 以复习锥形渐变的工作原理。
简单的解决方案
规范现在包含了 三角函数和反三角函数,这可能对我们有所帮助——对角线与垂直线的夹角是纵横比 atan(var(--ratio))
的反正切(矩形的左边缘、上边缘和对角线形成一个直角三角形,其中对角线与垂直线形成的角的正切是宽除以高——正好是我们的纵横比)。
将其转换为代码,我们有
--ratio: 3/ 2;
aspect-ratio: var(--ratio);
--angle: atan(var(--ratio));
background:
/* below the diagonal */
conic-gradient(from var(--angle) at 0 100%,
#319197, #ff7a18, #af002d calc(90deg - var(--angle)), transparent 0%),
/* above the diagonal */
conic-gradient(from calc(.5turn + var(--angle)) at 100% 0,
#ff7a18, #af002d, #319197 calc(90deg - var(--angle)));
但是,目前没有浏览器实现三角函数和反三角函数,因此简单的解决方案只是一个未来的解决方案,而不是一个在今天实际可行的解决方案。
JavaScript 解决方案
当然,我们可以在 JavaScript 中从 --ratio
值计算 --angle
。
let angle = Math.atan(1/ratio.split('/').map(c => +c.trim()).reduce((a, c) => c/a, 1));
document.body.style.setProperty('--angle', `${+(180*angle/Math.PI).toFixed(2)}deg`)
但是如果使用 JavaScript 不行呢?如果我们真的需要一个纯 CSS 解决方案呢?好吧,这有点笨拙,但可以做到!
笨拙的 CSS 解决方案
这个想法来源于 SVG 渐变的一个特性,老实说,当我 第一次遇到 时,我发现它非常令人沮丧。
假设我们有一个渐变,它在 50%
处有一个急剧的过渡,从下到上,因为在 CSS 中,这是一个 0°
角的渐变。现在假设我们在 SVG 中有相同的渐变,并且我们将两个渐变的角度更改为相同的值。
在 CSS 中,它是
linear-gradient(45deg, var(--stop-list));
在 SVG 中,我们有
<linearGradient id='g' y1='100%' x2='0%' y2='0%'
gradientTransform='rotate(45 .5 .5)'>
<!-- the gradient stops -->
</linearGradient>
如下所示,这两个结果并不相同。虽然 CSS 渐变确实在 45°
处,但旋转了相同 45°
的 SVG 渐变在对角线上具有橙色和红色之间的急剧过渡,即使我们的盒子不是正方形,因此对角线也不在 45°
处!

45°
CSS 与 SVG 渐变(演示)。这是因为我们的 SVG 渐变绘制在一个 1x1
的正方形盒子中,旋转了 45°
,这使得橙色到红色的突然变化沿着正方形的对角线。然后将此正方形拉伸以适应矩形,这基本上改变了对角线角度。
请注意,只有在我们不将 linearGradient
的 gradientUnits
属性从其默认值 objectBoundingBox
更改为 userSpaceOnUse
时,才会发生这种 SVG 渐变失真。
基本思路
我们不能在这里使用 SVG,因为它只有线性渐变和径向渐变,没有锥形渐变。但是,我们可以将 CSS 锥形渐变放在一个正方形盒子中,并使用 45°
角使它们沿对角线交汇
aspect-ratio: 1/ 1;
width: 19em;
background:
/* below the diagonal */
conic-gradient(from 45deg at 0 100%,
#319197, #ff7a18, #af002d 45deg, transparent 0%),
/* above the diagonal */
conic-gradient(from calc(.5turn + 45deg) at 100% 0,
#ff7a18, #af002d, #319197 45deg);
然后我们可以使用缩放 transform
来拉伸这个正方形盒子——诀窍在于,当用作 aspect-ratio
值时,“/”是分隔符,但在 calc()
中会被解析为除法。
--ratio: 3/ 2;
transform: scaley(calc(1/(var(--ratio))));
您可以尝试在下面的可编辑代码嵌入中更改 --ratio
的值,以查看这样,两个锥形渐变始终沿对角线交汇。
请注意,此演示仅在支持 aspect-ratio
的浏览器中有效。此属性在 Chrome 88+(当前版本为 90)中开箱即用,但 Firefox 仍然需要将 about:config
中的 layout.css.aspect-ratio.enabled
标志设置为 true
。如果您使用的是 Safari……好吧,抱歉!

此方法的问题以及如何解决
但是,缩放实际的 .card
元素很少是一个好主意。在我的用例中,卡片位于网格上,在它们上面设置方向性缩放会弄乱布局(即使我们在其中缩放了 .card
元素,网格单元格仍然是正方形)。它们还具有文本内容,这些内容会被 scaley()
函数奇怪地拉伸。

解决方案是为实际的卡片提供所需的 aspect-ratio
,并使用绝对定位的 ::before
(使用 z-index: -1
)放置在文本内容后面以创建我们的 background
。此伪元素获取其 .card
父元素的 width
,并且最初是正方形。我们还在其上设置了之前提到的方向性缩放和锥形渐变。请注意,由于我们的绝对定位的 ::before
与其 .card
父元素的上边缘对齐,因此我们也应该相对于此边缘对其进行缩放(transform-origin
沿 y 轴需要值为 0
,而 x 轴值无关紧要,可以是任何值)。
body {
--ratio: 3/ 2;
/* other layout and prettifying styles */
}
.card {
position: relative;
aspect-ratio: var(--ratio);
&::before {
position: absolute;
z-index: -1; /* place it behind text content */
aspect-ratio: 1/ 1; /* make card square */
width: 100%;
/* make it scale relative to the top edge it's aligned to */
transform-origin: 0 0;
/* give it desired aspect ratio with transforms */
transform: scaley(calc(1/(var(--ratio))));
/* set background */
background:
/* below the diagonal */
conic-gradient(from 45deg at 0 100%,
#319197, #af002d, #ff7a18 45deg, transparent 0%),
/* above the diagonal */
conic-gradient(from calc(.5turn + 45deg) at 100% 0,
#ff7a18, #af002d, #319197 45deg);
content: '';
}
}
请注意,在这个例子中,我们已经从 CSS 切换到了 SCSS。
这要好得多,正如下面的嵌入所示,它也是可编辑的,因此您可以使用 --ratio
并查看在您更改其值时所有内容如何很好地适应。
填充问题
由于我们没有在卡片上设置 padding
,因此文本可能会一直延伸到边缘,甚至由于略微倾斜而超出边界。

padding
导致问题。这应该不难解决,对吧?我们只需要添加 padding
,对吧?好吧,当我们这样做时,我们发现布局坏了!

padding
会破坏布局。(演示)这是因为我们设置在 .card
元素上的 aspect-ratio
是由 box-sizing
指定的 .card
盒子的纵横比。由于我们没有显式设置任何 box-sizing
值,因此其当前值是默认值 content-box
。在该盒子周围添加相同值的 padding
会得到一个具有不同纵横比的 padding-box
,它不再与 ::before
伪元素的纵横比一致。
为了更好地理解这一点,假设我们的 aspect-ratio
是 4/ 1
,并且 content-box
的宽度是 16rem
(256px
)。这意味着 content-box
的高度是此宽度的四分之一,计算结果为 4rem
(64px
)。因此,content-box
是一个 16rem×4rem
(256px×64px
)的矩形。
现在假设我们在每个边缘添加 1rem
(16px
)的 padding
。因此,padding-box
的宽度为 18rem
(288px
,如上动画 GIF 中所示)——计算为 content-box
的宽度,即 16rem
(256px
)加上来自 padding
的左侧 1rem
(16px
)和右侧 1rem
。类似地,padding-box
的高度为 6rem
(96px
)——计算为 content-box
的高度,即 4rem
(64px
),加上来自 padding
的顶部 1rem
(16px
)和底部 1rem
)。
这意味着padding-box
是一个18rem×6rem
(288px×96px
)的矩形,并且由于18 = 3⋅6
,它的纵横比为3/ 1
,这与我们为aspect-ratio
属性设置的4/ 1
值不同!同时,::before
伪元素的宽度等于其父元素的padding-box
宽度(我们计算为18rem
或288px
),并且它的纵横比(通过缩放设置)仍然是4/ 1
,因此它的视觉高度计算为4.5rem
(72px
)。这就解释了为什么使用此伪元素创建的background
——垂直缩放到18rem×4.5rem
(288px×72px
)的矩形——现在比实际卡片——现在带有padding
的18rem×6rem
(288px×96px
)矩形——更短。
因此,解决方案看起来非常简单——我们需要将box-sizing
设置为border-box
来解决我们的问题,因为这将aspect-ratio
应用于此盒子(当我们没有border
时,与padding-box
相同)。
当然,这修复了问题……但仅在 Firefox 中!

文本应垂直居中对齐,因为我们已为.card
元素提供了网格布局并在其上设置了place-content: center
。但是,这在 Chromium 浏览器中不会发生,当我们删除最后一个声明时,它会变得更加明显——不知何故,卡片网格中的单元格也获得了3/ 1
的纵横比,并溢出了卡片的content-box

place-content: center
(DEMO)。幸运的是,这是一个已知的 Chromium 漏洞,可能会在未来几个月内得到修复。
在此期间,我们可以做的是从.card
元素中删除box-sizing
、padding
和place-content
声明,将文本移动到子元素中(或者如果它只是一行并且我们很懒,则移动到::after
伪元素中,尽管如果我们希望文本保持可选择,则实际的子元素是更好的选择),并将其设置为带padding
的grid
。
.card {
/* same as before,
minus the box-sizing, place-content and padding declarations
the last two of which which we move on the child element */
&__content {
place-content: center;
padding: 1em
}
}
圆角
假设我们也希望我们的卡片具有圆角。由于像在创建我们background
的::before
伪元素上的scaley
这样的方向transform
也会扭曲角圆角,因此结果是实现此目的的最简单方法是在实际的.card
元素上设置border-radius
,并使用overflow: hidden
剪切圆角之外的所有内容。

但是,如果在某些时候我们希望.card
的其他一些后代在它外部可见,这就会变得有问题。因此,我们要做的是直接在创建卡片background
的::before
伪元素上设置border-radius
,并在该border-radius
的y分量上沿y轴反转方向缩放transform
$r: .5rem;
.card {
/* same as before */
&::before {
border-radius: #{$r}/ calc(#{$r}*var(--ratio));
transform: scaley(calc(1/(var(--ratio))));
/* same as before */
}
}
最终结果
将所有内容整合在一起,这是一个交互式演示,允许通过拖动滑块更改纵横比——每次滑块值更改时,都会更新--ratio
变量
你好,Ana
只是想知道为什么不能使用三角函数方法,因为在等待函数得到支持的同时,我们可以使用简单的数学近似代替。
替换
为
在“简单解决方案”示例中。
参见 https://codepen.io/villepreux/pen/ZEebJdV
—
最好的
这是一个 atan() 版本,如果您感兴趣,它也适用于纵横比 > 1 https://codepen.io/t_afif/pen/JjEwPoW。
是的,当我最初看到这个想法时,我也转向了复杂数学方法……
好的,首先,对于任何没有工程/数学背景的人来说,这是
atan(x)
函数的泰勒级数的粗略近似。为了回答你的问题……我今天只是觉得在纯 CSS 中这样做不方便?确实我过去曾在 Sass 中这样做,当时 Compass 没有提供反三角函数,但是……
1) 那是在四分之三个世纪前……
2) 我有更多的编码自由/在 Sass 中编码更容易——我可以创建自己的自定义函数并在任何需要的地方重用它,这意味着我不必每次需要计算给定其反正切值的角时都编写那个香肠一样的公式。我甚至不必编写一次,因为我会在循环中生成它。Sass 还允许我解决公式精度不足的点(如果在你的演示中将
--ratio
更改为5/4
或更大,你会看到结果不再是期望的结果),并且在纯 CSS 中实现该错误保护(虽然完全可能)只会使香肠更长。为什么不为此使用 JS?CSS 的发明是为了样式,用它进行计算很糟糕且丑陋。我想,现在很少有人在网站上禁用 JS。