图解CSS: Grid布局(Part13)

发布于 大漠

在 CSS 中,早期给元素设置宽高比都是通过一些 CSS Hack 来完成,比如padding-top(或 padding-bottom)的值为元素宽高比的百分比。不过自 Chrome90、Firefox88 和 Safar Technology Preview 之后,可以使用原生的 CSS 宽高比属性 aspect-ratio

在这一节中将和大家探讨宽高比(aspect-ratio)在 CSS 网格中的运用。在开始之前,先简单的回忆一下 CSS 的 aspect-ratio

CSS 的 aspect-ratio

一般来说,aspect-ratio 是一个相当弱的声明。

弱声明是指它在 CSS 的样式规则中,在某些条件之下是有可能不生效,或被别的属性覆盖。

也就是说,aspect-ratio 生效是要在一定条件之下才能生效,如果不满足这些条件,那么aspect-ratio将不生效(即使没有被aspect-ratio覆盖)。比如说下面这个示例:

.box {
    width: 100px;
    height: 50px;
    aspect-ratio: 16 / 9;
}

上面的代码告诉我们,盒子.box有一个固定的宽度(100px)和高度(50px),即盒子自身的宽高比是 100 : 50 = 2 : 1。即使盒子显式设置了aspect-ratio的值为16 / 9(宽高比是 16 / 9),但事实上盒子的宽高比是由它的宽度和高度决定了。简单地说,CSS的 aspect-ratio并没有生效。

把上面的案例再扩展一下:

.box {
    aspect-ratio: 16/9;
}

.fixed__height {
    height: 100px;
}

.fixed__width {
    width: 200px;
}

.fixed__height__width {
    width: 200px;
    height: 100px;
}

拖动示例中的滑块,改变容器的宽度,效果如下:

在这个示例中,四个盒子.box 都显式设置了aspect-ratio: 16/9;,但:

  • 第一个盒子未显式设置 widthheight 的值,即widthheight的值都是默认值 auto,容器有容宽,他就有多宽
  • 第二个盒子显式设置了 height 值为100pxwidth值为auto
  • 第三个盒子显式设置了 width 值为200pxheight 值为 auto
  • 第四个盒子显式设置了 width 值为 200pxheight 值为 100px

其中:

  • 第一个盒子的宽度等于容器的宽度,而高度根据 aspect-ratio: 16 / 9 的比率进行计算,当容器宽度变化时,盒子自己的宽度也会变化,对应其高度也会按 16:9比例调整
  • 第二个盒子的高度是固定的,它的宽度会根据16:9比例进行调整,但盒子大小是固定的
  • 第三个盒子和第二个盒子类似,不同的是其盒子宽度是固定的,它的高度会根据16:9比例进行调整,同样盒子大小是固定的
  • 第四个盒子中宽高都是固定的,而且根据宽高可以得出其比率是 200 : 100,即 2 / 1,此时 aspect-ratio的值并不生效,被忽略了

其实 aspect-ratio 中还有其他的一些细节,这里不做过多阐述,如果你对 aspect-ratio 感兴趣的话,可以阅读《使用CSS的 aspect-ratio 实现宽高比缩放》一文。

网格中的宽高比

接下来我们来看网格中的宽高比。

通过前面的学习,我们知道可以使用 grid-template-columnsgrid-template-rows 来显式设置网格轨道的尺寸。先来看第一种情况,即 使用grid-template-columns显式指定网格列轨道的尺寸,网格行轨道尺寸由网格项目宽高比来决定。比如:

.grid__container {
    display: grid;
    grid-template-columns: 100px 200px 300px;
}

.grid__item {
    aspect-ratio: 16 / 9;
}

就该示例而言,网格列轨道的尺寸分别是 100px(第一列)、200px(第二列)和 300px(第三列):

默认情况对应的网格项目宽度应该和所在列的网格轨道是相等的:

在网格项目上显式设置aspect-ratio: 16 / 9;之后,按照对aspect-ratio的理解,每个网格项目的高度应该是:

  • 第一个网格项目: 100 × 9 ÷ 16 = 56.25,即 56.25px
  • 第二个网格项目: 200 × 9 ÷ 16 = 112.5,即 112.5px
  • 第三个网格项目: 300 × 9 ÷ 16 = 168.75,即 168.75px

但放到 CSS 网格布局中,实际效果并不是我们所想的那样:

网格行轨道的尺寸以第三列网格轨道的aspect-ratio计算得来,即168.75px, 这个和我们期望的是相同的。同时其他网格项目的高度以行网格轨道为基准(在没有任何对齐属性设置的情况之下),不幸运的是,其他的网格项目(此例的第一个和第二个网格项目)的宽度会以新得出的网格行轨道尺寸结合aspect-ratio的比例重新计算其宽度。这就是为什么示例中所有网格项目的宽度都变成了300px,而且造成了网格项目的重叠。

如果我们把上面示例中的列网格轨道值重新调整,即第三列列轨道换成50px

.grid__container {
    display: grid;
    grid-template-columns: 100px 200px 50px;
}

此时你会发现,现在行轨道的尺寸将会是第二列列轨道尺寸200px结合aspect-ratio计算出来的值,即112.5px,同时第一个网格项目和第三个网格项目高度为112.5px,宽度重新按aspect-ratio计算得来:

我尝试着在该示例基础上新增了一个网格项目,此时构建了一个隐式的网格,第四个网格项目另起一行排列,但非常奇对的是,这个网格项目的尺寸是基于第一列网格轨道尺寸100px,并且按aspect-ratio计算出隐式行网格轨道尺寸62.5px,该网格项目(第四个网格项目)的尺寸也变成了 100px x 62.5px

按此方式继续新增一个网格项目,当有新的网格项目放置在第三列时,将和前面描述的现象等同:

从这几个基本案例我们可以知道一个基本现象:如果所有网格项目显式设置了宽高比aspect-ratio值,并且没有显式设置行网格轨道尺寸,那么行网格轨道将会是最大列网格轨道尺寸按aspect-ratio计算出来的值,同时其他小于最大列网格轨道所在列的网格项目会以新计算出来的行网格轨道值为基数,再按aspect-ratio值计算出网格项目的宽度,会造成网格项目的重叠

在上面示例基础上,在网格容器上显式设置grid-auto-columns来设置列网格轨道尺寸,并且grid-auto-flowcolumn

.grid__container {
    display: grid;
    grid-template-columns: 100px 200px 300px;
    grid-auto-columns: 50px;
    grid-auto-flow: column;
}

.grid__item {
    aspect-ratio: 16 / 9;
}

你将会发现,隐式的列网格轨道尺寸会是grid-auto-columns属性指定的值(比如该例中的50px),而网格项目的宽度会按前面的原理重新计算:

如果我们同时在网格容器上使用grid-template-columnsgrid-teplate-rows 设置列网格轨道和行网格轨道尺寸,结果又会怎么样呢?比如:

.grid__container {
    display: grid;
    grid-template-columns: 100px 200px 300px;
    grid-template-rows: 150px;
}

.grid__item {
    aspect-ratio: 16 / 9;
}

你将发现,此时网格轨道尺寸和grid-template-columnsgrid-template-rows显式设置的值一样,但网格项目的高度和网格行轨道的值150px相等,并且所有网格项目宽度基于150px按照aspect-ratio比率重新计算出来,都是150 × 16 ÷ 9 = 266.67

上面示例中行网格轨道尺寸小于最大列网格尺寸且大于最小列网格尺寸,如果把上面示例行网格轨道尺寸设置的比最大列网格轨道的尺寸还要大,又会是什么样的一个情景:

.grid__container {
    display: grid;
    grid-template-columns: 100px 200px 300px;
    grid-template-rows: 350px;
}

.grid__item {
    aspect-ratio: 16 / 9;
}

最终结果的计算是一样的,所有网格项目的高度等于行网格轨道尺寸350px,宽度基于该值按aspect-ratio重新计算,最终是 622.22px

继续添加条件,就上例而言,如果在网格容器上设置align-itemsstart,你会发现计算方式发生了变化,网格项目宽度对应的所在列的网格轨道尺寸,再根据aspect-ratio计算出网格项目的高:

但这并不表示align-items的值都会按这个方式进行计算,你可以尝试着改变align-items的值,你将看到下面这样的效果:

注意,在所有网格项目上显式设置align-self的值和在网格容器上显示设置align-items效果等。如果仅在单个网格项目上设置aslign-self,那么只有该网格项目会按上面这个方式计算。

再回过头来,看前面的示例:

.grid__container {
    display: grid;
    grid-template-columns: 100px 200px 300px;
}

.grid__item {
    aspect-ratio: 16 / 9;
}

这次在网格容器上显式设置justify-items的值为start。你会发现,网格项目的宽度变成了网格项目的内容宽度(即max-content),高度会以这个宽度为基数,再按aspect-ratio比率计算出网格项目的高度:

当网格项目内容不同时,那将为以最大内容的网格项目所占的宽度为基准,然后计算:

在这种条件之下,如果网格项目的内容宽度大于或等于网格列轨道最大尺寸时,将会回到之前的计算方式。

同样的,justify-items并不是所有值都会是按start这样的方式来处理网格项目:

注意,在所有网格项目中显式设置justify-self的效果和在网格容器上设置justify-items效果等同,如果只在单个网格项目上显式设置justify-self的话,只有该网格项目按这个方式计算。

虽然justify-itemsalign-itemsjustify-selfalign-self会对网格项目宽高比计算产生变化,但是justify-contentalign-content却不会产生任何影响,只会产生不同的对齐方式。

上面我们示例的效果都是 Chrome 91(版本 91.0.4472.164)中的效果,实测下来,几个主流浏览器效果都不相同:

从上图中的效果来看,只有Firefox浏览器下的效果才是我们所期望的效果(符合宽高比计算)。

有意思的是,在这种情况之下,我们在网格项目上显式设置height的值为min-contentmax-contentfit-content三个值中的一个,就能达到我们所期望的效果:

.grid__container {
    display: grid;
    grid-template-columns: 100px 200px 300px;
}

.grid__item {
    aspect-ratio: 16 / 9;
    height: min-content; // 或 max-content 或 fit-content
}

在上面示例中增加grid-template-rows: 400px,同样在网格项目显式设置heightmin-contentmax-contentfit-content将达到我们预期的效果,网格项目高度会根据网格列轨道和aspect-ratio比率计算出来:

.grid__container {
    display: grid;
    grid-template-columns: 100px 200px 300px;
    grid-template-rows: 400px;
}

.grid__item {
    aspect-ratio: 16 / 9;
    height: min-content; // 或 max-content 或 fit-content
}

是不是很神奇,说实话,其中原委我也还没整明白。

接下来,我们再来看另一个示例:

section {
    min-width: 50vw;
}

.grid__container {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    --width: 100%;
    width: var(--width);
}

.grid__item:nth-child(1) {
    aspect-ratio: 16 / 9;
}

.grid__item:nth-child(2) {
    aspect-ratio: 4 / 3;
}

.grid__item:nth-child(3) {
    aspect-ratio: 2 / 1;
}

视窗在1200px下,网格容器的宽度大约为600px,根据fr单位计算原理,可以获得没个网格列的轨道宽大约为200px:

示例中,每个网格项目的aspect-ratio值都不同,分别是:

  • 网格项目一是16 / 9
  • 网格项目二是4 / 3
  • 网格项目三是2 / 1

按常规理解,每个网格项目的宽度应该等于网格列轨道尺寸,在该示例都应该是 200px。如果按照 aspect-ratio比例来计算网格项目的高,他们应该是:

  • 网格项目一:200 × 9 ÷ 16 = 112.5,即112.5px
  • 网格项目二:200 × 3 ÷ 4 = 150,即150px
  • 网格项目三:200 × 1 ÷ 2 = 100,即100px

其中网格项目二的高度值最大,即150px。在这样的场景中,如果网格容器未显式设置网格行轨道的尺寸话,那么三个网格项目的高度值最大的一个将会成为行网格轨道的尺寸,所以上图中展示也告诉我们网格行轨道的尺寸为 150px。由于网格行轨道尺寸是150px,加上网格容器和网格项目上未显式设置任何对齐相关的属性,以及未在网格项目上显式设置height的值为min-contentmax-contentfit-content。根据前面的示例可以得知,网格项目一和网格项目二的宽度将会重新计算,即基于新得到的行网格轨道尺寸(150px)和aspect-ratio比率计算:

  • 网格项目一宽度:150 × 16 ÷ 9 = 266.67,即266.67px
  • 网格项目三宽度:150 × 2 ÷ 1 = 300,即300px

如果该示例,不做其他设置,仅改变网格容器自身宽度的话,也只会改变网格列轨道尺寸,但三个网格项目根据最初的计算得到的高度依然会不同,最终表现形式还是和上图一样,只是因为值的变化而有所差异:

如果在上面的示例中的所有网格项目上显式设置height的值为min-contentmax-contentfit-content,最终效果将会回到我们期望的效果:

即:

  • 网格项目一:200 × 9 ÷ 16 = 112.5,即112.5px
  • 网格项目二:200 × 3 ÷ 4 = 150,即150px
  • 网格项目三:200 × 1 ÷ 2 = 100,即100px

从上面示例中我们可以发现,如果希望网格项目宽高比按照期望进行计算,需要在网格项目显式设置height的值为非auto的值,而且当其值为min-contentmax-contentfit-content时,会基于列网格轨道尺寸计算;如果<length><length-percentage> 值,则会以height值为基数,再根据宽高比重新计算网格项目宽度

前面和大家提到过,aspect-ratio 是一个弱声明属性:

  • 元素的宽高都是auto时,其宽度会根据父容器大小调整,高度会基于宽度,且根据aspect-ratio的比率计算得出
  • 元素的高度是固定的,它的宽度会根据aspect-ratio比例进行调整,
  • 元素的宽度是固定的,它的高度会根据aspect-ratio比例进行调整
  • 元素的宽高都是固定的,会根据现有宽高得出元素的宽高比,此时 aspect-ratio的值并不生效,被忽略

基于该特性,把上面示例中的height换成width

能得到相应的效果,不同的是基于指定的widthaspect-ratio比率计算出网格项目高度。

同样的,如果同时在网格项目上指定宽高值,将会完全忽略aspect-ratio

实现网格项目宽高比其他方式

前面我们主要和大家探讨的是网格布局中网格项目自身显式设置aspect-ratio实现网格项目宽高比,但从上面的一些案例中来看,不难发现有些浏览器还是不足够完美。如果现在你希望在 CSS 网格布局中要让网格项目具有宽高比的效果,也不是件难事。因为也可以在网格布局中使用以前那些 CSS Hacks 实现宽高比的技术方案。接下来,我们简单看看这些 Hack 在 CSS 网格布局中的使用。

padding-top/bottom实现网格项目的宽高比

这个方案其实也很简单,只需要确保网格项目的宽度和所在网格区域宽度一样(即网格项目的width100%),然后在网格项目中使用一个伪元素来处理宽高比(padding-toppadding-bottom)。

<!-- HTML -->
<div class="grid__item" style="--aspect-ratio: 16 / 9"> 16 / 9 </div>
<div class="grid__item" style="--aspect-ratio: 4 / 3;"> 4 / 3 </div>
<div class="grid__item" style="--aspect-ratio: 2 / 1;"> 2 / 1 </div>

/* CSS */
.grid__container {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    place-items: start; /* 这个很关键 也可以在所有网格项目上使用place-self替代 */ 
}

.grid__item {
    width: 100%; /* 这个很关键 */
}

.grid__item::before {
    content: "";
    display: block;
    width: 1px;
    height: 0;
    padding-bottom: calc(100% / (var(--aspect-ratio))); /* 也可以是 padding-top */
}

CSS 自定义属性和 calc() 实现网格项目的宽高比

我在很多场景提到过, CSS的自定义属性(CSS变量) 是非常强大的一种CSS特性,特别是和CSS函数中的 calc()函数 的结合,将会有更多的无限可能。比如说,使用CSS自定义属性和calc()也能实现网格项目的宽高比的效果。只是这种方案可能不是在所有情况下都适用,因为它取决于是否知道外部容器的宽度,而且(因为它是基于使用calc()来计算网格行轨道尺寸)这必须昌一个固定值或视口单位,不能是一个百分比。一般来说,我不建议用这种方案来实现网格项目宽高比,特别是在响应式布局中,我们往往不知道网格项目的确切宽高。然而,在使用网格布局时,我发现大多数情况之下都知道网格容器的容器是一个固定宽度。

暂且不做过多场景的考虑,只看 CSS 网格布局中如何使用 CSS 自定义属性和 calc() 来实现网格项目的宽高比。在使用这种技术方案的时候,我们几个先决条件必须满足:

  • 网格容器的容宽度是已知的
  • 网格沟槽(网格轨道间距)是已知的
  • 网格列轨道数量已知
  • 想要实现的网格项目的宽高比已知

我们可以把这些需要的已知条件都转变成CSS的自定义属性:

:root {
    --grid__container--wrapper: 100vw; // 网格容器的宽度
    --gutter: 10px; // 网格沟槽(网格轨道之间的间距)
    --no__of--columns: 4; //网格轨道的数量
    // 网格项目的宽高比 
    --ratio__width: 1;
    --ratio__height: 1;
}

将这些已声明好的变量运用到网格容器上:

.grid__container {
    width: var(--grid__container--wrapper);
    display: grid;
    grid-template-columns: repeat(var(--no__of--columns), 1fr);
    gap: var(--gutter);
}

你将看到效果如下:

网格容器宽度(--grid__container--wrapper)和网格轨道之间的间距(--gutter)的值是已知的。但网格行轨道尺寸(假设是--grid__row--height)是未知的,需要通过--grid__container--wrapper--gutter计算得来。对于网格项目宽高比是1:1计算--grid__row-height相对简单一点:

:root {
    --grid__container--wrapper: 100vw; // 网格容器的宽度
    --gutter: 10px; // 网格沟槽(网格轨道之间的间距)
    --no__of--columns: 4; //网格轨道的数量
    // 计算网格行轨道尺寸
    --grid__row--height: calc(
        (var(--grid__container--wrapper) - (3 * var(--gutter))) / 4 // 4是网格列轨道数量,3是网格列轨道之间的沟槽数
    );
    // 网格项目的宽高比 
    --ratio__width: 1;
    --ratio__height: 1;
}

.grid__container {
    width: var(--grid__container--wrapper);
    display: grid;
    grid-template-columns: repeat(var(--no__of--columns), 1fr);
    gap: var(--gutter);
    grid-auto-rows: var(--grid__row--height);
}

此时,所有网格项目的宽和高都是相等的(宽高比为1:1)。在一些网格项目上使用grid-columngrid-row 来指定网格项目的位置:

.grid__item:nth-child(1) {
    grid-column: span 2;
    grid-row: span 2;
}

.grid__item:nth-child(5) {
    grid-column: 3 / span 2;
    grid-row: span 2;
}

.grid__item:nth-child(4),
.grid__item:nth-child(7) {
    grid-column: span 2;
}

.grid__item:nth-child(9) {
    grid-column: span 4;
}

这个时候你看到效果,网格会中会有缺口出现:

阅读过前面内容的话,我们知道可以在网格容器上设置grid-auto-flow的值为dense可以让网格项目自动放置到相应的缺口中:

上面这个示例,有明显一个特征,那就是知道网格有多少列网格轨道(四列),这样就知道有具体的多少个沟槽(4 - 1= 3)。所以在计算--grid__row--height的时候,知道列数为4,列沟槽数为3。如果希望网格更灵活一些,就需要用一个自定义属性(比如--no__of--gutters):

:root {
    --grid__container--wrapper: 100vw; // 网格容器的宽度
    --gutter: 10px; // 网格沟槽(网格轨道之间的间距)
    --no__of--columns: 4; //网格轨道的数量
    --no__of--gutters: calc(var(--no__of--columns) - 1); // 网格列沟槽数量
    // 计算网格行轨道尺寸
    --grid__row--height: calc(
        (var(--grid__container--wrapper) - (var(--no__of--gutters) * var(--gutter))) / var(--no__of--columns) // 4是网格列轨道数量,3是网格列轨道之间的沟槽数
    );
    // 网格项目的宽高比 
    --ratio__width: 1;
    --ratio__height: 1;
}

你可能发现了,上面示例中定义的--ratio__width--ratio__height并没有使用上,这主要是因为我们的宽高比是1:1。如果我们想改变网格项目的宽高比的时候(比如16:94:3),这两个变量就变得很有意义了。当网格项目宽高比不是1:1时,网格行轨道的尺寸计算也会变得更为复杂:

:root {
    --grid__container--wrapper: 100vw; // 网格容器的宽度
    --gutter: 10px; // 网格沟槽(网格轨道之间的间距)
    --no__of--columns: 4; //网格轨道的数量
    --no__of--gutters: calc(var(--no__of--columns) - 1); // 网格列沟槽数量
    
    // 网格项目的宽高比 
    --ratio__width: 1;
    --ratio__height: 1;
    // 宽高比系数(比率)
    --factor: calc(var(--ratio__height) / var(--ratio__width));

    // 计算网格行轨道尺寸
    --grid__row--height: calc(
        ((var(--grid__container--wrapper) - (var(--no__of--gutters) * var(--gutter))) / var(--no__of--columns)) * var(--factor)
    );
}

注意,虽然这种方式能实现网格项目的宽高比的效果,但其局限性和不确定性也是很多。在实际使用的时候不建议采用这种方式。就个人而言,我还是推建使用aspect-ratio来实现网格项目的宽高比效果。