网格布局

本章概要

  • 使用网格构建页面布局
  • 理解网格布局选项
  • 使用网格线和具名网格区域放置子元素
  • 显式与隐式网格
  • 结合 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 所示:

图 5.3 网格的组成部分
图 5.3 网格的组成部分

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

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

图 5.4 用网格创建的网页布局效果图。虚线标出了每个网格单元的位置

图 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 所示。

图 5.5 基础网格布局生效后的示例页效果图
图 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 就能将每个元素指定到具体位置。

图 5.7 网格线从左上角的 1 开始编号;负数则是从右下角的 -1 开始编号

图 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 号垂直网格线之间),纵向上分属第三行和第四行网格单元。

注意

这些属性其实是简写属性: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 所示。

图 5.8 Flexbox 只在一个方向上对齐各元素,而网格则在两个方向上同时对齐
图 5.8 Flexbox 只在一个方向上对齐各元素,而网格则在两个方向上同时对齐

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

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

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

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

图 5.9 示例页面最终效果图
图 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 所示的布局效果:

图 5.10 网格元素放置的位置为:从第二个名为“col”的网格线开始,向右横跨两个网格轨道的位置(即 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 号网格线之间),网格就会添加其他轨道将其包含在内。

图 5.11 若网格元素置于定义的显式网格轨道外,则会创建隐式轨道将该元素包含在内
图 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 样式类的图片,并添加一些样式细节来完成最终布局。

图 5.13 图片在基础网格中从左至右排布
图 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)。