网格布局

本章概要

  • 使用网格构建页面布局
  • 理解网格布局选项
  • 使用网格线和具名网格区域放置子元素
  • 显式与隐式网格
  • 结合 Flexbox 和网格购买连贯的网页布局

上一章介绍的 Flexbox 彻底颠覆了传统的网页布局方式,但也只能算作 CSS 布局全貌的一部分。它还有一个大哥:一个被称作网格布局模块(Grid Layout Module,即 Grid 网格) 的新规范。这两个规范共同提供了一套功能齐全的布局引擎。

本章将重点演示网格布局的用法,先概括介绍网格布局的工作原理,然后结合几个示例带您感受一下网格布局所具备的能力。它既可以轻松拿捏基础网格的构建,也具备足够强悍的实力搞定各种复杂布局,但后者必须掌握额外的新属性和关键字。本章将步步为营详细介绍,助您攻克这些知识点。

CSS 网格先是定义了一个由不同的行与列所构成的二维布局,然后再将页面元素分别放置到对应的网格中。一些元素可能只占据一个网格单元,另一些元素则可能跨越多列或多行。网格的尺寸大小既可以精确定义,也可以根据自身内容自动计算。元素既可以选择精确放置到网格某个位置,又可以在网格内自动定位,自行填充划分好的区域。一套网格系统就足以构建出如图 5.1 所示的复杂布局。

Figure 5.1 Boxes in a sample grid layout
图 5.1 基础网格布局中的盒子

5.1 构建基础网格

网格的用途极其广泛,本章将通过几个示例来展示其强大功能。先来构建一个最基础的网格布局,分三列布局如图 5.2 所示的六个方框。页面对应的 HTML 标记详见代码清单 5.1。

Figure 5.2 A simple grid you’ll construct with three columns and two rows
图 5.2 包含三列两行的简单网格效果图

与上一章类似,新建一个页面并关联一个新的样式表,页面内容如下。在这段代码中,各方框分别从字母 a 到 f 进行编号,这样它们在网格中的位置就一目了然了。

代码清单 5.1 包含六个子元素的页面 HTML 标记

<div class="grid"><!-- 网格容器 -->
  <!-- 容器内的子元素变为网格元素 -->
  <div class="a">a</div>
  <div class="b">b</div>
  <div class="c">c</div>
  <div class="d">d</div>
  <div class="e">e</div>
  <div class="f">f</div>
</div>

跟 Flexbox 类似,网格布局也是作用在包含两个层级的 DOM 结构中。设置了 display: grid 的元素将成为一个 网格容器(grid container),其子元素则变为 网格元素(grid items)。

接下来,要用一些新的属性来定义网格的具体细节。按照如下代码更新样式表:

代码清单 5.2 基础网格布局

.grid {
  display: grid;  /* 将元素设为网格容器 */
  grid-template-columns: 1fr 1fr 1fr;  /* 定义三个等宽列 */
  grid-template-rows: 1fr 1fr;  /* 定义两个等高行 */
  gap: 0.5em;  /* 设置各网格单元格之间的间距 */
}
 
.grid > * {
  background-color: darkgray;
  color: white;
  padding: 2em;
  border-radius: 0.5em;
}

该样式分三列渲染了六个大小相同的方框(如图 5.2 所示)。这当中有好几个新的知识点,下面将详细介绍。

首先,display: grid 定义了一个网格容器,使该容器呈现出块级元素的行为特征,100% 占满可用宽度。另外也可以使用 inline-grid(没写到示例中),这样元素就会在行内流动,且宽度只能够包裹子元素,但 inline-grid 用得并不多。

接下来是新属性:grid-template-columns 和 grid-template-rows。这两个属性定义了网格每行每列的大小。本例使用了一种新单位 fr,代表每一列(或每一行)的 分数单位(fraction unit)。这个单位的作用跟 Flexbox 中的 flex-grow 属性极为相似。声明 grid-template-columns: 1fr 1fr 1fr 则设置了三个大小相同的三个等宽列。

属性值不一定非得用分数单位 fr,像 px、em 或百分数这样的单位也可以使用;也可以混搭组合使用。例如,grid-template-columns: 300px 1fr 就定义了一个固定宽度为 300px 的首列,以及一个紧跟其后的第二列,后者会填满剩余的可用空间。此外,2fr 对应的列宽是 1fr 的两倍。

和 Flexbox 布局一样,gap 属性则定义了每个网格单元之间的间距。也可以用两个值分别指定垂直及水平方向的间距(例如:gap: 0.5em 1em)。

注意
网格规范首次定稿时,gap 属性曾被命名为 grid-gap,因此早期的一些网格案例用的是 grid-gap,其工作方式相同。后来添加了更为通用的 gap 属性,并同步更新了 Flexbox 予以支持。

可以试着改改这些属性值,看看它们会对最终布局产生什么影响。试试再加一列,或者改改宽度,又或者添加或删除网格元素……本章后续布局也会继续这样试验,这是掌握新东西最好的方法。

5.2 网格结构剖析

了解网格的各个部分非常重要。前面已经学习了 网格容器网格元素,它们是网格布局的基本要素。还有四个重要概念如图 5.3 所示:

Figure 5.3 The parts of a grid
图 5.3 网格的组成部分

构建网格布局时会涉及到这些组成部分。例如,声明 grid-template-columns: 1fr 1fr 1fr 就能定义三个等宽且垂直的 网格轨道,同时还定义了四条垂直的的 网格线:一条位于网格的最左边;另外两条位于每个网格轨道之间,还有一条则位于网格的最右边。

上一章我们用 Flexbox 构建过一个示例页。不妨再回过头去看看当时的设计,考虑一下该怎样用网格布局再来实现一版。总体设计如图 5.4 所示,虚线标出了每个网格单元的位置。注意,某些区域跨越了好几个网格单元,即填充了更大的 网格区域

Figure 5.4 Page layout created with grid. The dashed lines are added to indicate the location of each grid cell.
图 5.4 用网格创建的网页布局效果图。虚线标出了每个网格单元的位置

上面的网格布局包含四行两列,其中前两个水平网格轨道分别是页面标题(Ink)部分和主导航菜单部分。主区域填满了第一个垂直轨道剩余的两个网格单元,而侧边栏的两个板块则分置于第二个垂直轨道剩余的两个网格单元内。

说明
布局设计无需填满每一个网格单元。在想留白的地方空出对应的网格单元即可。

使用 Flexbox 布局时,必须按照一定的方式去嵌套元素。第 5 章我们先用 Flexbox 定义了两列,然后在右侧边栏嵌套了另一个 Flexbox 来定义两个子板块所在的行(详见代码清单 5.1)。要用网格实现同样的布局效果,就得改改页面的 HTML 结构:将嵌套的 HTML 拉平,使得放置在网格内的每个页面元素都必须是主网格容器(main grid container)的子元素。新的 HTML 标记如代码清单 5.3 所示。创建一个新页面,并按以下代码更新页面内容(或者直接修改第五章中的示例页)。

代码清单 5.3 网格布局对应的 HTML 结构

<body>
  <div class="container"><!-- 这里的“容器”即网格容器 -->
    <header><!-- 每个网格元素都必须是网格容器的子元素 -->
      <h1 class="page-heading">Ink</h1>
    </header>
 
    <nav><!-- 每个网格元素都必须是网格容器的子元素 -->
      <ul class="site-nav">
        <li><a href="/features">Features</a></li>
        <li><a href="/pricing">Pricing</a></li>
        <li><a href="/support">Support</a></li>
        <li class="nav-right">
          <a href="/about">About</a>
        </li>
      </ul>
    </nav>

    <main class="main tile"><!-- 每个网格元素都必须是网格容器的子元素 -->
      <h1>Team collaboration done right</h1>
      <p>Thousands of teams from all over the
        world turn to <b>Ink</b> to communicate
        and get things done.</p>
    </main>

    <div class="sidebar-top tile"><!-- 每个网格元素都必须是网格容器的子元素 -->
      <form class="login-form">
        <h3>Login</h3>
        <p>
          <label for="username">Username</label>
          <input id="username" type="text"
            name="username"/>
        </p>
        <p>
          <label for="password">Password</label>
          <input id="password" type="password"
            name="password"/>
        </p>
        <button type="submit">Login</button>
      </form>
    </div>

    <div class="sidebar-bottom tile centered stack"><!-- 每个网格元素都必须是网格容器的子元素 -->
      <small>Starting at</small>
      <div class="cost">
        <span class="cost-currency">$</span>
        <span class="cost-dollars">20</span>
        <span class="cost-cents">.00</span>
      </div>
      <a class="cta-button" href="/pricing">
        Sign up
      </a>
    </div>
  </div>
</body>

新版页面将所有内容区域都变成了网格元素:标题、菜单(nav)、主区域外加两个侧边栏。主区域和两个侧边栏都加上了 tile 样式类,因为它们都是白色背景,也有相同的内边距。

接着对新页面应用网格布局,并将各部分内容指定到对应区域。稍后我们将基于第五章的这版示例页引入大量新的样式,现在不妨先看看网格生效后的页面渲染情况,如图 5.5 所示。

Figure 5.5 Page with basic grid structure in place
图 5.5 基础网格布局生效后的示例页效果图

然后新建一张样式表,并关联到该页面。样式内容如代码清单 5.4 所示:

代码清单 5.4 最外层设置的网格布局样式

*,
::before,
::after {
  box-sizing: border-box;
}
 
:root {
  --gap-size: 1.5rem;
}
 
body {
  background-color: #709b90;
  font-family: Helvetica, Arial, sans-serif;
}
 
.stack > * + * {
  margin-block-start: 1.5em;
}
 
.container {
  display: grid;
  grid-template-columns: 2fr 1fr;  /* 定义两个垂直的网格轨道 */
  grid-template-rows: repeat(4, auto);  /* 定义四个大小为 auto 水平网格轨道 */
  gap: var(--gap-size);
  max-inline-size: 1080px;   
  margin-inline: auto;
}
 
header,
nav {
  grid-column: 1 / 3;  /* 垂直网格线从1号线跨越至3号线 */
  grid-row: span 1;  /* 恰好跨越一条水平轨道 */
}
 
.main {
  /* 将其他网格元素定位到不同的网格线之间 */
  grid-column: 1 / 2;
  grid-row: 3 / 5;
}
 
.sidebar-top {
  /* 将其他网格元素定位到不同的网格线之间 */
  grid-column: 2 / 3;
  grid-row: 3 / 4;
}

.sidebar-bottom {
  /* 将其他网格元素定位到不同的网格线之间 */
  grid-column: 2 / 3;
  grid-row: 4 / 5;
}
 
.tile {
  padding: 1.5em;
  background-color: #fff;
}
 
.tile > :first-child {
  margin-top: 0;
}

这段样式代码引入了很多新的写法,下面来逐个击破——

首先对 .container 设置了网格容器,并用 grid-template-columnsgrid-template-rows 定义了网格轨道。因为列的分数单位分别为 2fr1fr,所以第一列的宽度是第二列的两倍。定义行的时候用到了一个新方法,即 repeat() 函数,用于简化多个网格轨道的声明。

声明 grid-template-rows: repeat(4, auto) 定义了四个高度为 auto 的水平网格轨道。这种写法相当于声明 grid-template-rows: auto auto auto auto 。轨道大小指定为 auto,表示轨道尺寸将根据自身内容进行调整。

repeat() 简化表示法还可以用来定义不同的重复模式,比如 repeat(3, 2fr 1fr) 会重复三遍 2fr 1fr,从而定义出六个网格轨道,重复的结果为 2fr 1fr 2fr 1fr 2fr 1fr,效果如图 5.6 所示。

图 5.6 在网格模板定义里使用 repeat() 函数定制重复模式示意图
图 5.6 在网格模板定义里使用 repeat () 函数定制重复模式示意图

还可以将 repeat() 作为更长的模式的一部分进行简化。例如,grid-template-columns: 1fr repeat(3, 3fr) 1fr 定义了一个 1fr 宽的列,后面是连续三个宽度为 3fr 的列,最后又是一个宽 1fr 的列(即 1fr 3fr 3fr 3fr 1fr)。不难发现,完整版的模板定义乍一看未必直观,因此才有了 repeat() 这样的简化写法。

5.2.1 网格线的编号

网格轨道定义好后,下一步就是将各网格元素放置到特定的位置。浏览器给网格中的每条网格线都分配了如图 5.7 所示的编号。有了它们 CSS 就能将每个元素指定到具体位置。

Figure 5.7 Grid lines are numbered beginning with 1 on the top left. Negative numbers refer to the position from the bottom right.
图 5.7 网格线从左上角的 1 开始编号;负数则是从右下角的 -1 开始编号。

元素位置的指定需要用到 grid-columngrid-row 这两个属性。若网格元素是沿垂直方向从 1 号网格线跨越到 3 号线,则设置为 grid-column: 1 / 3;若是沿水平方向从 3 号网格线跨越到 5 号线,则设置为 grid-row: 3 / 5。这两个属性一起就能将元素放置到指定的网格区域。

示例页中的网格元素也是按如下样式摆放到位的:

.main {
  grid-column: 1 / 2;
  grid-row: 3 / 5;
}

.sidebar-top {
  grid-column: 2 / 3;
  grid-row: 3 / 4;
}

.sidebar-bottom {
  grid-column: 2 / 3;
  grid-row: 4 / 5;
}

这段代码将 main 元素放到了第一列(即 1 号和 2 号垂直网格线之间),并纵向跨越第 3 ~ 4 行(即 3 号与 5 号水平网格线之间)。侧边栏的两个内容板块则都放置在靠右那一列(即 2 号与 3 号垂直网格线之间),纵向上分属第三行和第四行网格单元。

Note

这些属性其实是简写属性:grid-columngrid-column-startgrid-column-end 的简写形式;而 grid-row 则是 grid-row-startgrid-row-end 的简写形式。中间的斜线仅用于区分简写属性中的两个属性值,斜线前后的空格不做强制要求。

而定位 headernav 的规则集略有不同。本例使用了相同的规则集同时定位这两个元素:

header,
nav {
  grid-column: 1 / 3;
  grid-row: span 1;
}

上述样式用了前面介绍的 grid-column 将元素铺满整个网格的宽度。其实还可以用另一个特殊关键字 span 来设置 grid-rowgrid-column(这里用在了 grid-row 上)。该关键字会告知浏览器元素需要占据一条网格轨道。由于未指明具体哪一行,因此网格元素会根据其 布局算法(placement algorithm ,自动放置放置到网格上可以容纳该元素的第一处可用空间,即本例的第一行与第二行。本章稍后会详细介绍该算法。

5.2.2 网格与 Flexbox 配合

学了网格布局,开发人员常常会问 Flexbox 和网格布局是否只能二选一。答案是大可不必。它们其实是互补关系。二者在很大程度上是一同开发出来的,虽然功能上存在部分重叠,但它们各自擅长的场景不一样。设计中究竟是用弹性盒布局还是网格布局,最终取决于具体的需求和应用场景。这两种布局方式有以下两个重要区别:

由于具备一维属性,Flexbox 更适合用在由相似元素组成的行(或列)上。尽管支持用 flex-wrap 换行,但 Flexbox 无法让上一行元素同下一行对齐。相反,网格的二维属性则很好地解决了上述问题,可以让一条轨道上的元素同另一条上的对齐。二者的效果对比如图 5.8 所示。

Figure 5.8 Flexbox aligns items in one direction, while grid aligns items in two directions.
图 5.8 Flexbox 只在一个方向上对齐各元素,而网格则在两个方向上同时对齐

根据 CSS WG 成员 Rachel Andrew 的观点,它们的第二个区别在于,Flexbox 是从内容出发的,而网格则从布局出发的。Flexbox 将一系列元素排布到一行或一列中,无需专门设置元素的尺寸大小,因为尺寸是根据自身内容决定的。

而在网格中,先要定义好布局,然后将各元素放到布局结构中。鉴于每个元素的内容都能影响所在网格轨道的尺寸,这样整个轨道尺寸也都将受其影响,进而波及到轨道内的其他元素大小。

示例页的主区域用网格来定位,是希望其内容能限制在它所在的网格内;而对于页面上的其他元素,如导航菜单,这样的限制则大可不必:元素文字多的也可以适当宽一点,文字少也可以窄一些。此外,它还是一个水平(一维)布局。因此首选 Flexbox 进行布局。接下来用 Flexbox 给这些元素分别设置布局,完成整个页面样式设计。

如图 5.9 所示,顶部导航菜单里的链接是水平对齐的。右下角报价板块的样式也用 Flexbox 进行了处理。加上这些布局和少量的其他样式后,示例页的最终样式就完成了。

Figure 5.9 Fully styled page
图 5.9 示例页面最终效果图

除了整体的布局是用网格实现的(如代码清单 5.4 所示),其余样式都跟第四章一样,这里直接复用。根据以下样式更新示例页。

代码清单 5.5 剩余的页面样式

.page-heading {
  margin: 0;
}
 
.site-nav {
  display: flex;  /* 用 Flexbox 处理导航菜单 */
  gap: var(--gap-size);
  margin: 0;
  padding: 0.5em;
  background-color: #5f4b44;
  list-style-type: none;
}
 
.site-nav > li > a {
  display: block;
  padding: 0.5em 1em;
  background-color: #cc6b5a;
  color: white;
  text-decoration: none;
}
 
.site-nav > .nav-right {
  margin-inline-start: auto;
}
 
.login-form h3 {
  margin: 0;
  font-size: 0.9em;
  font-weight: bold;
  text-align: right;
  text-transform: uppercase;
}
 
.login-form input:not([type="checkbox"]):not([type="radio"]) {
  display: block;
  width: 100%;
}
 
.login-form button {
  margin-block-start: 1em;
  border: 1px solid #cc6b5a;
  background-color: white;
  padding: 0.5em 1em;
  cursor: pointer;
}
 
.centered {
  text-align: center;
}
 
.cost {
  display: flex;  /* 用 Flexbox 处理价格部分 */
  justify-content: center;
  align-items: center;
  line-height: 0.7;
}
 
.cost-currency {
  font-size: 2rem;
}
.cost-dollars {
  font-size: 4rem;
}
.cost-cents {
  font-size: 1.5rem;
  align-self: flex-start;
}
 
.cta-button {
  display: block;
  background-color: #cc6b5a;
  color: white;
  padding: 0.5em 1em;
  text-decoration: none;
}

当设计要求元素在两个维度上同时对齐时,首选网格布局;若只考虑单一维度上的元素排布问题,则选用 Flexbox 布局。实践中,这通常(并非绝对)意味着网格更适用于整体的页面布局,而 Flexbox 则更适合网格区域内的特定元素布局。网格和 Flexbox 布局用得多了,对于不同场景下该用什么样的布局方式自然就游刃有余了。

注意
网格布局和 Flexbox 布局都能避免元素间发生外边距折叠(margin collapsing)。随着间隙 gap 的设置,通过用户代理引入的外边距可能偶尔会在元素间产生多余的间距。这也是示例样式中好几处将外边距重置为 0 的根本原因。

5.3 两种替代语法

Alternate syntaxes

布局网格元素还有另外两个替代语法:命名网格线与命名网格区域,具体选用哪种写法视个人喜好而定。在某些设计中,一种语法可能较另一种更好理解。本节将分别介绍这两种语法。

5.3.1 命名网格线

Naming grid lines

有时候记录所有网格线的编号未免过于繁琐,尤其是在网格轨道很多的时候。为了能简单点,可以给网格线命名,并在布局时使用该名称而非其编号。定义网格轨道时,可以在任意两个轨道间添加一对中括号,写上网格线的名称,如以下代码片段所示:

grid-template-columns: [start] 2fr [center] 1fr [end];

这条声明定义了一个双列布局的网格,同时命名了三条垂直网格线,分别为 startcenterend。之后就可以用这些名称来声明网格元素放置的位置,不用再去数网格编号了。例如:

grid-column: start / center;

上述声明将网格元素放置在了 1 号网格线(即 start)与 2 号网格线(即 center)这间的区域。此外,同一网格线还可以有多个名称,如以下声明所示(这里对代码做了换行处理,以增强可读性):

grid-template-columns:
  [left-start] 2fr
  [left-end right-start] 1fr
  [right-end];

该声明中,2 号网格线既叫作 left-end 又叫作 right-start,使用时任选其一即可。这里还有一个设置技巧:将网格线命名为 left-startleft-end,相当于定义了一个位于二者之间的、名称为 left 的区域。这里的后缀 -start-end 某种意义上充当了声明该区域的关键字。如果给网格元素设置 grid-column: left,则指定了一个从网格线 left-start 延展到 left-end 的区域。

使用命名网格线来布局示例页的新样式代码,如代码清单 5.6 所示,效果与代码清单 5.4 相同。按以下代码更新示例页面:

代码清单 5.6 使用命名网格线实现的网格布局代码

.container {
  display: grid;
  grid-template-columns:
    /* 分别给每条垂直网格线命名 */
    [left-start] 2fr
    [left-end right-start] 1fr
    [right-end];
  /* 将水平网格线命名为 row */
  grid-template-rows: repeat(4, [row] auto);
  gap: var(--gap-size);
  max-inline-size: 1080px;
  margin-inline: auto;
}

header,
nav {
  grid-column: left-start / right-end;
  grid-row: span 1;
}

.main {
  grid-column: left; /* 跨越 left-start 到 left-end 之间的区域 */
  grid-row: row 3 / span 2; /* 从第三个命名网格线 row 开始放置元素,并跨越两个网格轨道 */
}

.sidebar-top {
  grid-column: right; /* 跨越 right-start 到 right-end 之间的区域 */
  grid-row: 3 / 4;
}

.sidebar-bottom {
  grid-column: right; /* 跨越 right-start 到 right-end 之间的区域 */
  grid-row: 4 / 5;
}

上述样式利用手动命名的垂直网格线,将每一个元素放置在相应的网格列内;而水平网格线的命名则是由 repeat() 函数实现的,最终这些水平网格线除了最后一条外,其余都被命名为了 row。这看起来可能很奇怪,但像这样重复使用同一个名称来命名也是有效的。这样一来,main 元素就被放置在了从 row 3 开始的位置(即第三条名为 row 的水平网格线),并由此(沿垂直编码方向向下)跨越两个网格轨道。

DIY 补充说明:关于 repeat(4, [row] auto) 的含义

根据命名网格线的定义,具体的名称要写在任意两个网格轨道之间,所以这里的 repeat(4, [row] auto) 展开后相当于 [row] auto [row] auto [row] auto [row] auto。由此可见,repeat() 函数定义了四个网格轨道行,每行宽度均为 auto,并且每行“顶部”的那条网格线都被命名为了 row。起初学到这里时,我曾将 auto 理解成了网格线名称的默认值,是不对的。正确的理解是将 auto 视为网格轨道。如下图 Chrome 浏览器的开发者工具所标注的网格所示:

补图1 除了最后一条水平网格线,其余都被命名为 “row”

补图 1 除了最后一条水平网格线,其余都被命名为 “row”
补图2 实测 Chrome 浏览器对左上角两个不同方向的命名网格线分别做了标注,并用箭头符号加以区分

补图 2 实测 Chrome 浏览器对左上角两个不同方向的命名网格线分别做了标注,并用箭头符号加以区分

命名网格线的用法数不胜数,具体怎么用,还要结合每个网格特定的结构才能确定。比如实现一个如图 5.10 所示的布局效果:

Figure 5.10 Placing a grid item at the second "col" grid line, spanning two tracks (col 2 / span 2)

图 5.10 网格元素放置的位置为:从第二个名为“col”的网格线开始,向右横跨两个网格轨道的位置(即 col 2 / span 2)

该场景展示了另一种重复模式的写法:网格列按每两列为一组,然后对每组前方那条垂直网格线统一命名(即 grid-template-columns: repeat(3, [col] 1fr 1fr)),接着再用命名的网格线将元素定位到第二组网格列上(即 grid-column: col 2 / span 2)。

5.3.2 命名网格区域

Naming grid areas

另一个替代语法是对网格区域进行命名。该语法既不用去数网格线的编号,也不用对网格线命名;定位元素时直接将其关联到命名的网格区域中即可。使用时需要借助另外两个属性的共同参与,即 网格容器grid-template-area 属性和 网格元素grid-area 属性。

代码清单 5.7 给出了该写法的一个示例。最终的布局效果还是跟之前的示例页(即代码清单 5.4 和 5.6)完全一样。它是一种替代语法。根据如下代码更新示例页:

代码清单 5.7 使用命名的网格区域

.container {
  display: grid;
  grid-template-areas:
    /* 将每个网格单元分配到一个命名的网格区域中 */
    "title title"      
    "nav   nav"        
    "main  aside1"     
    "main  aside2";    
  grid-template-columns: 2fr 1fr; /* 跟之前一样定义网格轨道的尺寸大小 */
  grid-template-rows: repeat(4, auto); /* 跟之前一样定义网格轨道的尺寸大小 */
  grid-gap: var(--gap-size);
  max-inline-size: 1080px;
  margin-inline: auto;
}

header {
  grid-area: title; /* 将每个网格元素放到一个命名的网格区域 */
}

nav {
  grid-area: nav; /* 将每个网格元素放到一个命名的网格区域 */
}

.main {
  grid-area: main; /* 将每个网格元素放到一个命名的网格区域 */
}

.sidebar-top {
  grid-area: aside1; /* 将每个网格元素放到一个命名的网格区域 */
}

.sidebar-bottom {
  grid-area: aside2; /* 将每个网格元素放到一个命名的网格区域 */
}

grid-template-area 属性使用了一种类似 ASCII 字符画风格(ASCII art 的语法,可以直接在 CSS 中绘制出一个可视化的网格示意图。声明中给出了一系列带引号的字符串,每个字符串分别代表网格中的某一行,其中各列则用空格分隔。

本例中,第一行全部分给了网格区域 title,第二行则给了 nav;接下来的两行,左边一列分给了主区域 main,右边侧边栏的两个子板块则分别分配给了 aside1aside2。就这样,每个网格元素通过 grid-area 属性被放置到了对应的命名区域中。

警告

每个命名的网格区域必须组成一个矩形,CSS 不允许出现更复杂的形状,例如 L 形或 U 形。

还可以用句点(.)作为名称,这样就能空出相应的网格单元。例如下面的样式声明,其中定义了四个网格区域,它们都环绕在中间那个留白的网格单元周围:

grid-template-areas:
  "top  top    right"
  "left .      right"
  "left bottom bottom";

鉴于网格布局设计了三种语法,即带编号的网格线、命名网格线、命名网格区域,在构建网格布局时,就选那个用得最顺手的语法即可。最后一个是众多开发者的最爱,特别是在明确知道每个网格元素的位置的情况下,这种写法的优势尤为明显。

5.4 显式网格与隐式网格

Explicit and implicit grid

在某些场景下,您可能并不知道该把元素放在网格的哪个具体位置上。遇到网格元素特别多的情况,挨个去指定元素的确切位置未免太过麻烦;更有甚者,遇到页面元素是从数据库动态获取的话,其数量就更无法预判了。针对这些实际情况,改用一种宽松的方式去定义网格、再让布局算法去处理网格元素的定位问题,不失为一种更合理的变通方案。

这就需要用到 隐式网格(implicit grid。前面用形如 grid-template-* 的属性定义出的网格轨道,其实是 显式网格(explicit grid。但有些网格元素仍然可以放到显式轨道的外面,此时会自动创建隐式轨道,让扩展后的网格包含这些元素。

在如图 5.11 所示的网格中,显式轨道在两个方向上都只定义了一个。当把网格元素放在横竖第二个轨道上时(即 2 号和 3 号网格线之间),网格就会添加其他轨道将其包含在内。

Figure 5.11 If a grid item is placed outside the declared grid tracks, implicit tracks will be added to the grid until it can contain the item.
图 5.11 若网格元素置于定义的显式网格轨道外,则会创建隐式轨道将该元素包含在内

隐式网格轨道的默认大小为 auto,即根据网格元素内容进行自动延展;若要对所有隐式网格手动设置大小,则需要指定网格容器上的 grid-auto-columnsgrid-auto-rows 属性(如 grid-auto-columns: 1fr)。

注意
引用网格线时,隐式网格轨道不会改变负数值的含义。负的网格线编号仍然是从显式网格的右下方开始的。

本节将实现一个新页面布局来演示隐式网格的用法。该页面是一个摄影作品墙,如图 5.12 所示。该布局需要对列设置网格轨道,而网格行则是隐式创建的。这样页面就不会关注图片的固定数量,从而适应任意数量的网格元素。只要照片需要换行显示,就会隐式地新增一行:

图 5.12 使用隐式网格行对一组照片进行网格布局
图 5.12 使用隐式网格行对一组照片进行网格布局

这个布局的独特之处在于,无论是换用 Flexbox 布局还是浮动布局,都很难实现同样的页面效果。这个示例充分体现了网格布局的强大。

要实现这样的布局效果,需要一个新页面。创建一个空白页面,然后关联到另一个新的空白样式表。根据代码清单 5.8 所示的 HTML 标记更新空白页:

代码清单 5.8 摄影作品墙的 HTML 标记

<!doctype html>
<head>
  <link href="styles.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="portfolio">
  <figure class="featured"><!-- 每个<figure>都是一个网格元素 -->
    <img src="images/monkey.jpg" alt="monkey" />
    <figcaption>Monkey</figcaption>
  </figure>
  <figure>
    <!-- 图片和标题封装在<figure>元素内部 -->
    <img src="images/eagle.jpg" alt="eagle" />
    <figcaption>Eagle</figcaption>
  </figure>
  <figure class="featured"><!-- 用样式类 featured 标记较大的图片 -->
    <img src="images/bird.jpg" alt="bird" />
    <figcaption>Bird</figcaption>
  </figure>
  <figure>
    <img src="images/bear.jpg" alt="bear" />
    <figcaption>Bear</figcaption>
  </figure>
  <figure class="featured"><!-- 用样式类 featured 标记较大的图片 -->
    <img src="images/swan.jpg" alt="swan" />
    <figcaption>Swan</figcaption>
  </figure>
  <figure>
    <img src="images/elephants.jpg" alt="elephants" />
    <figcaption>Elephants</figcaption>
  </figure>
  <figure>
    <img src="images/owl.jpg" alt="owl" />
    <figcaption>Owl</figcaption>
  </figure>
</div>
</body>
</html>

这段 HTML 标记包含一个类名为 portfolio 的元素(作网格容器),以及一系列 figure 元素(即网格元素)。每个 figure 又包含一张照片和一个标题。其中部分元素带有样式类 featured,表示该元素后续会放大展示。

接下来,我会分几个阶段来演示上述布局的实现过程。首先,创建网格轨道,让图片以基础网格布局的形式进行展示(如图 5.13 所示)。之后,考虑放大带 featured 样式类的图片,并添加一些样式细节来完成最终布局。

Figure 5.13 Items automatically placed in grid cells from left to right
图 5.13 图片在基础网格中从左至右排布

上述布局的样式代码如下代码清单 5.9 所示。该代码使用 grid-auto-rows 给所有的隐式网格行设置了 1fr 的大小,每一行都具有相同的高度。该布局还引入了两个新概念:auto-fillminmax() 函数,稍后会进一步介绍。先将这段代码更新到新建的样式表中:

代码清单 5.9 用了隐式网格行的网格样式

body {
  background-color: #709b90;
  font-family: Helvetica, Arial, sans-serif;
}
 
.portfolio {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));  /* 将最小列宽设置为 200px 并自动填充网格 */
  grid-auto-rows: 1fr;  /* 将隐式水平网格轨道的大小设为 1fr */
  grid-gap: 1em;
}
 
.portfolio > figure {
  margin: 0;  /* 覆盖掉浏览器默认的外边距样式 */
}
 
.portfolio img {
  max-inline-size: 100%;
}
 
.portfolio figcaption {
  padding: 0.3em 0.8em;
  background-color: rgb(0 0 0 / 0.5);  /* 半透明黑色 */
  color: #fff;
  text-align: right;
}

有时,网格轨道的尺寸不是固定的,但却要求限制在一个特定范围内。此时就要用到 minmax() 函数。它接受两个参数,分别是最小值和最大值。浏览器将确保网格轨道的尺寸介于两者之间。(如果最大尺寸小于最小尺寸,则忽略该最大尺寸)通过设置 minmax(200px, 1fr),所有网格轨道的宽度都至少为 200px

repeat() 函数中的关键字 auto-fill 是一个特殊的属性值。这样设置后,只要网格还装得下,浏览器就会在指定的范围内(即 minmax() 的值)尽可能多地生成网格轨道。

auto-fillminmax(200px, 1fr) 配合,则表示网格会在可用的空间内尽可能多地产生网格列,并确保所有的列宽均不少于 200px。由于任何轨道宽度都不能大于 1fr(即指定的最大值),因此所有网格轨道都是等宽的。

在如图 5.13 所示的页面中,浏览器视口可以容纳四个宽 200px 的列,因此一行有四个网格轨道。如果视口变宽,就能放下更多轨道;一旦收窄,产生的轨道数也会相应减少。

注意
如果设置了 auto-fill,而网格元素又不足以填满该行所有的网格轨道,就会出现一些空的网格轨道。如果不希望这样,可以换用关键字 auto-fitauto-fit 会让不为空的轨道延展开来,直到填满可用空间。有关这两个关键字的区别,请参阅:https://gridbyexample.com/examples/example37/

auto-fillauto-fit 究竟选谁,得看网格填充时的具体需求:是希望轨道尺寸固定但数量不固定(即 auto-fill),还是希望数量固定但尺寸不固定(即 auto-fit)。

5.4.1 添加变化

Adding variety

接下来,让作品墙中的特写图片(如本例中的小鸟和天鹅)放大些来增强视觉趣味性。放大前的每个网格元素都各自占据了 1 × 1 的区域。然后将特写图片的尺寸增加到 2 × 2,方法是通过样式类 featured 选中特写元素,并让它们在水平和垂直方向上都占据两个网格轨道的大小。

问题来了:由于元素按从左至右的顺序排列,放大某些网格元素将导致网格中出现空白区域,如图 5.14 所示。小鸟图之前在第三个网格元素内,但因为尺寸变大了,老鹰图的右侧单元格已经容纳不下这张图片,因此只能掉到下一行的网格轨道。

图 5.14 增加某些网格元素的尺寸大小会导致布局中出现无法容纳大元素的空白区域
图 5.14 增加某些网格元素的尺寸大小会导致布局中出现无法容纳大元素的空白区域

当不主动设置网格元素的位置时,元素会按照默认的布局算法(placement algorithm)自行定位。默认情况下,布局算法会尝试按元素在 HTML 标记中的顺序逐列、逐行摆放。当一个元素在某一行放不下时(即该元素占据了太多网格轨道时),布局算法会将其移动到下一行,寻找足够大的空间来安置它。于是本例中的小鸟图就被挪到了下方第二行,放到了老鹰图的下面。

网格布局模块(Grid Layout Module)还提供了另一个属性 grid-auto-flow 来控制布局算法的行为。它的初始值(initial value)为 row,效果就是上面截图看到的样子。如果设置为 column,布局算法就会将元素优先放在网格列中,并且只有等这一列也放不下时,才会移动到该行的下一列;直到这一行最后一列也不行时,才会考虑换到下一行,以此类推。

该属性还可以添加关键字 dense(如 grid-auto-flow: column dense)。这样,布局算法就能紧凑地填满网格里的空白,尽管会打乱某些网格元素的顺序。加上 dense 后,较小的图片元素就会“回填”到由大图片造成的空白区域,效果如图 5.15 所示:

图 5.15 grid-auto-flow 属性添加关键字 dense 后,小网格元素就能回填网格的空白区域
图 5.15 grid-auto-flow 属性添加关键字 dense 后,小网格元素就能回填网格的空白区域

让布局算法的自主流动(auto-flow)紧凑起来之后(即添加了 dense),放置大图片时留下的空白区域将由小图片优先补空。此时源码顺序还是不变(猴、老鹰、小鸟、熊),但最后那张熊图就被挪到了小鸟图前面,填补了第一行的空缺。

按照代码清单 5.10 更新样式表。改样式放大了特写图片,使其在水平和垂直方向均占据两个网格轨道,并启用了紧凑的自主流动模式。

代码清单 5.10 放大特写图片的样式代码

.portfolio {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  grid-auto-rows: 1fr;
  gap: 1em;
  grid-auto-flow: dense; /* 启用紧凑的网格布局算法 */
}

/* 放大特写图片,使其在水平和垂直方向上各占据两个网格轨道的大小 */
.portfolio .featured {
  grid-row: span 2;
  grid-column: span 2;
}

这段样式代码(第 6 行)使用了 grid-auto-flow: dense,相当于 grid-auto-flow: row dense(前者隐含了 row 这个初始值)。然后指定特写图片的尺寸为:在水平和垂直方向上各占据两个网格轨道大小。注意,本例中只用了 span 关键字,并没有具体指明什么元素该放到什么轨道,这样布局算法就会将网格元素放到它认为合适的位置。

实际屏幕看到的效果可能跟图 5.12 不完全一致,因为本例用 auto-fill 来确定垂直网格轨道的数量,所以屏幕越宽,可以容纳的轨道数就越多;屏幕越窄,相应的轨道数就越少。我截图时的宽度约为 1000px,能装下四个网格轨道。适当调整浏览器的窗口大小,就能看到网格自动生成了多少个轨道来填充可用空间。

需要注意的是,布局算法设置为紧凑型自主流动后,可能会导致元素显示的顺序跟 HTML 里的源码顺序不一致。当用户借助键盘(如 Tab 健)或屏幕阅读工具来浏览网页时,这些辅助工具会以 HTML 中的源码顺序而非屏幕渲染顺序来浏览网页,这样可能会让用户感到困惑。

5.4.2 让网格元素填满网格轨道

Adjusting grid items to fill the grid track

至此,您已经实现了相当复杂的页面布局,其间也没有花太多精力去设置每个元素的精确位置,而是将这些计算的活儿都交给了浏览器。

还剩最后一个问题:图片放大后并没有完全填满所在的网格单元,于是图片下方就空出一小段空白来。理想情况下,每个网格元素无论顶部还是底部,都应该与同一轨道上的其他元素对齐。现在顶部倒是对齐了,但底部却没有,如图 5.16 所示:

图 5.16 图片没能完全填满网格单元,留下了多余的空间

图 5.16 图片没能完全填满网格单元,留下了多余的空间

下面来解决这个问题。回想一下,每个网格元素都是一个 <figure>。它包含了一张图片和一个标题这两个子元素:

<figure class="featured">
  <img src="images/monkey.jpg" alt="monkey" />
  <figcaption>Monkey</figcaption>
</figure>

默认情况下,每个 网格元素 都会通过拉伸来填满整个网格区域,但 子元素 例外,从而空出了多余的高度。一个简单点的解决办法是使用 Flexbox 布局。如代码清单 5.11 所示,将每个 <figure> 元素都视为一个竖直方向上的弹性容器,内部元素就会从上到下垂直排列;接着再给图片指定一个 flex-grow 值,就可以强制拉伸图片来填满空白区域。

只不过拉伸图片并不可取,因为会改变图片的宽高比(height-to-width ratio),从而导致图片变形。好在 CSS 提供了一个特殊的属性 object-fit 来处理这个问题。默认情况下,一个 <img> 图片元素的 object-fit 的属性值为 fill,也就是通过缩放整个图片来填满 <img> 元素。您也可以改成其他值来进行调节。

例如,object-fit 的属性值还可以是 covercontain(如图 5.17 所示)。这些值会告诉浏览器,要在不改变图片纵横比(aspect ratio)的情况下,调整渲染容器中图片大小,具体用法如下:

图 5.17 使用 object-fit 控制图片在容器盒内的渲染方式
图 5.17 使用 object-fit 控制图片在容器盒内的渲染方式

这里一定要区分清楚两个概念:一个是宽高由 <img> 元素决定的容器盒(the box),另一个是要渲染的图片。默认情况下,二者的尺寸大小是相等的。object-fit 属性只能对容器盒内的图片尺寸进行控制,而容器盒本身的大小则保持不变。

因为要用 flex-grow 拉伸图片,这里应该给它加上 object-fit: cover 以防变形。这样会裁剪掉图像的一小部分边缘——这也是必要的妥协。最终效果如图 5.18 所示。有关 object-fit 属性的更多详情,可以参考 CSS-Tricks 网站的文章

图 5.18 所有图片都填满了网格区域并完美对齐后的效果图

图 5.18 所有图片都填满了网格区域并完美对齐后的效果图

至此,所有图片和标题的上下边缘都跟网格轨道对齐了,相关样式如代码清单 5.11 所示。请将它们更新到您的样式表中:

代码清单 5.11 使用垂直方向的 Flexbox 拉伸图片并填充网格区域的样式代码

.portfolio > figure {
  display: flex; /* 令每个网格元素都成为垂直的 Flexbox */
  flex-direction: column; /* 令每个网格元素都成为垂直的 Flexbox */
  margin: 0;
}

.portfolio img {
  flex: 1; /* 使用 flex-grow 让图片填满弹性容器的可用空间 */
  object-fit: cover; /* 允许图片在不被拉伸变形的情况下填满容器(而是裁掉边缘) */
  max-inline-size: 100%;
}

这样,您的摄影作品墙就大功告成了!所有元素都整整齐齐地排列在网格里,浏览器决定了垂直网格轨道的数量和尺寸大小。布局算法启用紧凑型自主流动(dense auto-flow)模式后,浏览器也可以干净利落地填满所有的空白区域。

5.5 子网格布局

Subgrid

在前面的示例中,网格布局都只作用于 DOM 结构的两个层级:一是网格容器,二是其(直接)子元素。而对于某些设计,可能需要在更大层级范围内实现元素对齐。例如,要对齐同一祖先元素内的两个子元素,或者对它们设置相同的大小。这类设计的一个示例效果,如图 5.19 所示:

图 5.19 内部元素对齐的多张卡片布局效果

图 5.19 内部元素对齐的多张卡片布局效果

上述样式设计中,多张卡片都位于同一个网格内,因此水平方向排列整齐,高度也都相同。不仅如此,不同卡片的内部元素也实现了相互对齐:标题较短的两张卡片,通过增加顶部空间来与长标题那张对齐;每张卡片的正文段落相互间也实现了首行水平对齐;“Read more”(即“阅读更多”)字样的超链接部分则实现了底边水平对齐。

要实现上述效果,最佳方案是利用一种全新的 CSS 特性,称为 子网格(subgrid。有了子网格,一个网格不仅能放入另一个网格,其网格元素还能放置在外层网格的网格线上。本例中,每张卡片都占据了三行网格空间,并且各自的内容都与其对应的行相互对齐。图 5.20 高亮标出了参与页面布局的网格及其网格间隙。

图 5.20 高亮标注的网格线与网格间隙效果图

图 5.20 高亮标注的网格线与网格间隙效果图

这时,就能看清卡片本身及其内部元素在同一个网格布局里进行定位的全过程了。

说明

图 5.20 是页面在 Firebox 浏览器中显示、并通过开发者工具 DevTools 标注出网格行的效果截图。在大多数浏览器中,想要调试出类似的网格效果,只需要打开检查面板(Inspector pane),然后找到目标元素旁边一个小小的“grid”字样的标签,单击它即可。Flexbox 布局也支持类似的功能。

需要注意的是,子网格当前还是 CSS 新出的一个功能特性,并不像网格那样得到广泛支持。虽然子网格出现在 Firefox 及 Safari 浏览器中已经有些时日了,但 Chrome 和 Edge 浏览器也是到 2023 年底才对该特性予以支持。关于子网格在浏览器中的最新支持情况,请参阅:https://caniuse.com/css-subgrid

下面来演示上述布局的构建过程。首先创建一个新页面,并根据代码清单 5.12 更新页面 HTML 标记:

代码清单 5.12 三张作者信息卡片的网格布局 HTML 代码

<!doctype html>
<html lang="en-US">
<head>
  <link href="styles.css" rel="stylesheet" type="text/css" />
</head>
<body>
  <div class="author-bios"><!-- 主网格容器 -->
    <div class="card"><!-- 卡片元素 -->
      <h3>Sir Arthur Ignatius Conan Doyle</h3>
      <div>
        <p>
          A British writer and physician who created
          the fictional detective Sherlock Holmes.
        </p>
      </div>
      <div class="read-more"><a href="/conan-doyle">
        Read more
      </a></div>
    </div>
    <div class="card"><!-- 卡片元素 -->
      <h3>Mark Twain</h3>
      <div>
        <p>
          An American author famous for <i>The
          Adventures of Tom Sawyer</i> and
          <i>Adventures of Huckleberry Finn</i>.
          He has been called “the father of
          American literature.”
        </p>
      </div>
      <div class="read-more"><a href="/mark-twain">
        Read more
      </a></div>
    </div>
    <div class="card"><!-- 卡片元素 -->
      <h3>Homer</h3>
      <div>
        <p>
          Author of the Greek epic poems <i>The
          Iliad</i> and <i>The Odyssey</i>.
        </p>
      </div>
      <div class="read-more"><a href="/homer">
        Read more
      </a></div>
    </div>
  </div>
</body>
</html>

如以上代码所示,实现该布局,需要重点处理好这三个层级的 DOM 元素对齐:

  1. 主网格容器:即 <div class="author-bios"> 元素;
  2. 各网格元素:即三个 <div class="card"> 元素;
  3. 卡片各自的子元素。

先来处理基础样式,包括背景色、间距、以及用于放置卡片的最顶层网格,如代码清单 5.13 所示。其中的样式可能除了个别网格声明还有点陌生(如果您正在熟悉网格布局)以外,其他大部分样式应该都已经很熟悉了:

代码清单 5.13 对作者卡片设置网格布局的样式代码

body {
  background-color: #eee;
  font-family: Helvetica, Arial, sans-serif;
}
 
.author-bios {
  display: grid;  /* 声明网格容器 */
  grid-template-columns: repeat(3, 1fr);  /* 定义容器中的三个等宽网格列 */
  gap: 1em;
  max-inline-size: 800px;
  margin-inline: auto;
}
 
.card {
  padding: 1rem;
  border-radius: 0.5rem;
  background-color: #fff;
}
 
.card > h3 {
  margin-block: 0;
  padding-block-end: 0.5rem;
  border-block-end: 1px solid #eee;
}
 
.read-more {
  margin-block-start: 1em;
  text-align: right;
}

以上样式生效后,三张卡片就会在水平方向一字排开,看上去已经非常接近最终效果了(如图 5.21 所示)。之后就剩子网格的具体设置了。

图 5.21 子网格生效前,三张作者信息卡的对齐效果图
图 5.21 子网格生效前,三张作者信息卡的对齐效果图

要在页面中添加子网格,必须先在网格元素中声明 display: grid,这样就在外层网格中创建了一个内部网格。接着,在该内部网格的 grid-template-rows 和/或 grid-template-columns 中使用关键字 subgrid,表示其使用的应该为父网格的网格线。

相关 CSS 样式如代码清单 5.14 所示。请将以下样式更新到本地样式表:

代码清单 5.14 为卡片内容设置子网格的样式代码

.card {
  display: grid;  /* 设置该卡片为网格容器 */
  gap: 0;
  grid-row: span 3; /* 令卡片横跨三行 */
  grid-template-rows: max-content auto max-content; /* 为不支持子网格的浏览器设置回退方案,指定网格行尺寸 */
  grid-template-rows: subgrid; /* 使用父网格的网格行 */
  padding: 1rem;
  border-radius: 0.5rem;
  background-color: #fff;
}

.card > h3 {
  align-self: end; /* 令标题内容对齐到网格行的底部 */
  margin-block: 0;
  padding-block-end: 0.5rem;
  border-block-end: 1px solid #eee;
}

鉴于子网格仍是 CSS 一个较新的功能特性,这里特地为 grid-template-rows 提供了一套回退方案来效仿子网格的行为特征(该方案只能将三个“Read more”链接相对网格底边统一对齐,无法让标题部分相对各自的底边对齐)。还有一种备选方案,是将这些样式放到 CSS 特性查询语法中,检测当前环境对子网格的支持情况,例如:@supports (grid-template-rows: subgrid) {…}

译注

关于上述回退方案的实际效果,原书一笔带过。这里觉得有必要配一张截图,方便与子网格的实际对齐效果进行比较:
补图1:当浏览器不支持子网格功能、备选回退方案生效时的页面效果

补图 1:当浏览器不支持子网格功能、备选回退方案生效时的页面效果

本例演示了关键字 subgrid 将元素对齐到父网格行的具体方法,您也可以声明 grid-template-columns: subgrid 让元素根据父网格的列对齐,甚至同时对行与列对齐。此外,还可以在 DOM 树中延续这种写法,将子网格嵌套进另一个子网格中。

5.5.1 子网格的其他配置项

Additional options

子网格中的网格线编号、网格线名称以及网格区域名称均从父网格中继承而来,因此可以利用它们在子网格中放置元素。例如,以下样式会将所有卡片标题放在第二行:

.card {
  grid-template-rows: subgrid [line1] [line2] [line3] [line4];
}

以上这些命名网格线,与前面 5.3.1 小节介绍的一样,它们的含义及用法与在普通网格上定义的网格线均相同。例如,声明 grid-row: line3 / line4 会将元素放置在第 3 条和第 4 条水平网格线之间的子网格中。

注意

未来可能出现的一个新网格特性功能,叫做 砖石布局(masonry layout)(译注:发音为 /ˈmeɪsənri/)。它是一种在相册场景下非常流行的布局方式,但需要借助 JavaScript 才能实现其效果。在砖石布局中,网格元素被放置在一系列宽度相等的网格列中,并且允许网格元素根据自身内容决定其高度,因此网格行不一定对齐。具体情况可以参考维也纳前端开发者 Manuel Matuzović 发表的这篇概述:Day 72: the masonry-auto-flow property

译注

为方便查阅,特将 5.3.1 节介绍命名网格线时自行补充的网格布局效果展示如下 :

补图1 除了最后一条水平网格线,其余都被命名为 “row”
补图 2 除了最后一条水平网格线,其余都被命名为 “row”

对应的样式设置如下(详见 5.3.1 节:命名网格线):

.container {
display: grid;
grid-template-columns:
 /* 分别给每条垂直网格线命名 */
 [left-start] 2fr
 [left-end right-start] 1fr
 [right-end];
/* 将水平网格线命名为 row */
grid-template-rows: repeat(4, [row] auto);
gap: var(--gap-size);
max-inline-size: 1080px;
margin-inline: auto;
}

header,
nav {
grid-column: left-start / right-end;
grid-row: span 1;
}

.main {
grid-column: left; /* 跨越 left-start 到 left-end 之间的区域 */
grid-row: row 3 / span 2; /* 从第三个命名网格线 row 开始放置元素,并跨越两个网格轨道 */
}

.sidebar-top {
grid-column: right; /* 跨越 right-start 到 right-end 之间的区域 */
grid-row: 3 / 4;
}

.sidebar-bottom {
grid-column: right; /* 跨越 right-start 到 right-end 之间的区域 */
grid-row: 4 / 5;
}

此外,关于上面提到的 砖石布局,也可以参考 MDN 的线上官方文档,这里只补充其中一个示例效果图:
补图3 MDN 官方文档给出的一个砖石布局效果图
补图 3 MDN 官方文档给出的一个砖石布局效果图

5.6 对齐相关的属性

Alignment properties

网格布局模块规范里的对齐属性有一些与 Flexbox 相同,还有一些则是新属性。上一章介绍 Flexbox 布局时已经涵盖了其中大部分内容,这一节就来看看这些属性在网格布局中的用法。想要对网格布局的各个方面做进一步控制,了解这些属性或许会方便很多。

CSS 给网格布局提供了三个以 justify- 开头的对齐属性:justify-contentjustify-items 以及 justify-self。它们控制了网格元素在水平方向上的位置。我是这样记的:就像在文字处理器里调整文字位置,让它们在水平方向上排布。

此外还有三个以 align- 开头的对齐属性:align-contentalign-items 以及 align-self。它们控制了网格元素在垂直方向上的位置。我是通过类比 行内对齐(inline alignment) 中的 vertical-align 属性来记住它们的。这些属性如图 5.22 所示。

图 5.22 网格内的对齐属性
图 5.22 网格内的对齐属性

要设置网格容器内的网格轨道在水平和垂直方向上的位置,可以使用 justify-contentalign-content 属性实现,尤其是在网格元素的尺寸无法填满网格容器的时候。参考以下样式代码:

.grid {
  display: grid;
  height: 1200px;
  grid-template-rows: repeat(4, 200px);
}

这段代码明确设置了网格容器的高度为 1200px,但水平网格轨道的有效总高度仅为 800px;网格轨道在剩下那 400px 的空间内如何分布,可以通过 align-content 属性进行设置。该属性可以设为下列有效值:

想了解更多对齐属性(justify/alignment properties)的示例,请访问 Grid by Example 是一个极好的网站资源,里面汇集的大量网格布局示例,都是由开发者兼 W3C 成员的 Rachel Andrew 大佬精心整理而成。

最后再来聊聊与定位相关的简写属性:place-contentplace-items 以及 place-self。这些属性可以同时声明带 align-*justify-* 前缀的属性值,例如:place-content: center start 等效于 align-content: center; justify-content: start

因为网格布局的内容非常多,所以本章介绍的内容都是网格布局必须掌握的核心概念。建议您对网格布局做更多探索试验。网格有很多种组合方式,无法在一章里逐一介绍,因此您需要自我挑战一下,去尝试一些新事物。在遇到一个有趣的网页布局时,看看能否用网格布局来实现。

译注

关于水平方向对齐的那三个 justify 开头的属性,作者提供的记忆方式可能有点抽象,这里略作补充。所谓的“文字处理器”,可以将其理解为微软旗下的办公软件 Word。它在对齐一行文本的时候,使用的术语就是 justify。根据上海译文出版社 2000 年 12 月出版的《新英汉词典》(世纪版),这个单词在印刷行业中的原意为“调整(铅字)的间隔使齐行”。对照如下图所示的 Word 中英双语提示信息,可以进一步加深理解(这也是设置文字两端对齐的快捷键选择 Ctrl + J 的根本原因):

补图1 微软办公软件 Word 就文字对齐给出的双语对照注释

补图 1 微软办公软件 Word 就文字对齐给出的双语对照注释

5.7 本章小结

Summary