非原创,翻译自An Interactive Guide to CSS Grid
建议结合阅读原文,原文示例可交互
CSS Grid 是 CSS 语言里最令人赞叹的部分之一,它为我们提供了许多新型工具,可以用这些工具来创建精致且流畅的布局。
它还出人意料地复杂。我花了相当长的时间才真正适应 CSS 网格!
本教程中,我将分享我在学习 CSS 网格布局时获得的最大 启发时刻。你将学习这种布局模式的基础知识,并了解如何用它做一些很酷的事情。✨
浏览器支持?
CSS Grid 是构建 CSS 布局最现代的工具,但它并不是 “前沿” 技术;它自 2017 年以来就受到所有主流浏览器的支持!
根据 caniuse 的数据,CSS Grid 拥有 97.8% 的用户支持率。支持程度非常高;Flexbox 的支持率也仅高出 0.5% 左右!
CSS 由多种不同的布局算法组成,每种算法都针对不同类型的用户界面而设计。默认布局算法流式布局(Flow layout)适用于数字文档。表格布局(Table layout)适用于表格数据。弹性盒布(Flexbox)局用于沿单个轴分布项目。
CSS 网格(Grid)是最新且最佳的布局算法。它非常强大:我们可以用它基于一些条件构建灵活调整的复杂布局。
在我看来,CSS Grid 最不寻常的部分是它仅使用 CSS 定义了网格结构(即行和列):
使用用 CSS Grid,可以将一个 DOM 节点细分为行与列。在本教程中,我们使用虚线高亮显示行/列,但实际上它们都是不可见的。
这真是太奇怪了!在其他所有布局模式中,创建这样的分隔区域唯一的方法就是增加更多的 DOM 节点。例如,在表格布局中,每一行都是用一个 <tr> 来创建的,而该行内的每个单元格则使用 <td> 或 <th> 来实现:
<table> <tbody> <!-- First row --> <tr> <!-- Cells in the first row --> <td></td> <td></td> <td></td> </tr> <!-- Second row --> <tr> <!-- Cells in the second row --> <td></td> <td></td> <td></td> </tr> </tbody></table>
不同于表格布局,CSS 网格允许我们完全在 CSS 内部管理布局。我们可以按照自己的意愿切割容器,创建出网格子元素可以作为锚点使用的分隔区域。
我们通过 display 属性选择进入网格布局模式:
.wrapper { display: grid;}
默认情况下,CSS 网格使用单列,并将根据子项的数量根据需要创建行。这被称为隐式网格,因为我们没有明确定义任何结构。
工作原理如下:
隐式表格是动态的,会根据子元素的数量添加或移除行。每个子元素都有它自己的行。
默认情况下,网格容器的高度由其子元素决定。它会动态地增长和缩小。有趣的是,这甚至不是“CSS 网格”的特性;网格容器仍然使用流式布局,而流式布局中的块级元素会垂直增长以包含它们的内容。只有子元素是使用网格布局来排列的。
但如果我们给网格设定一个固定的高度呢?在那种情况下,总面积会被划分成等大小的行:
默认情况下,CSS 网格会创建一个单列布局。我们可以使用 grid-template-columns 属性来指定列:
向 grid-template-columns 传入两个值 — 25% 和 75% — 我告诉 CSS 网格算法将元素切分成两列。
列可以用任何合法的 CSS 值来定义,包括像素、rem、视口单位等等。此外,我们还可以使用一个新的单位,也就是fr单位:
“fr”表示“fraction”。在这个示例中,我们说第一列应占据 1 个单位的空间,而第二列占据 3 个单位的空间。这意味着共有 4 个单位的空间,这将成为分母。第一列消耗了可用空间的 ¼,而第二列消耗了 ¾。
fr 单位为 CSS 网格带来了 Flexbox 风格的灵活性。百分比和 <length> 值创建固定约束,而 fr 列可以根据需要自由增长和缩小,以包含它们的内容。
请尝试增减该容器,以查看差别:
在这种场景下,我们的第一列有一个毛茸茸的小幽灵,显示的宽度明确为 55 像素。但如果列太小无法包含它该怎么办?
更确切地说,fr 分配的是多余空间。首先,列宽将根据其内容进行计算。如果有任何剩余空间,它将根据 fr 值进行分配。这与 flex-grow 非常相似,如我在 Flexbox 交互式指南 中所述。
总体而言,这种灵活性是一件好事。百分比太死板了。
我们可以通过 gap 看到这个原理的一个非常好的例子。gap 是一个神奇的 CSS 属性,它可以在网格中的所有列和行之间添加固定数量的间距。
看看当我们在百分比和小数间切换时会发生什么:
当使用百分比时,注意内容是如何溢出网格父元素的吗?这是因为百分比是使用网格的总面积计算的。两个列占用了父元素内容区域的 100%,并且不允许它们缩小。当我们添加 16px 的间距时,这些列别无选择,只能溢出容器。
相反,fr 单位是根据额外的空间来计算的。在这种情况下,额外的空间因 gap 而减少了 16 像素。CSS 网格算法会在两个网格列之间分配剩余的空间。
如果我们向一个两列的网格中添加超过两个子元素会发生什么?
好吧,我们来尝试一下:
有趣!我们的网格产生了新的行。grid算法想要确保每个子项都有它自己的网格单元格。它会根据需要生成新行,以实现这一目标。在项目数量变化的情况下(例如照片网格),且希望网格自动扩展时,这非常方便。
在其他情况下,我们想要显式的定义行,创建特定的布局。我们可以使用 grid-template-rows 属性来完成此操作:
通过定义 grid-template-rows 和 grid-template-columns,我们创建了一个明确的网格。这是构建页面布局的完美选择,例如本教程顶部的“圣杯”布局。
想象一下我们要创建一个日历:
CSS 网格是用于这种类型事情的非常棒的工具。我们可以将其构建为 7 列网格,每列占用 1 个单位的空间:
这种方法可行,但不得不数每个 1fr 项有点烦人。想象一下,如果我们有 50 列!
幸运的是,有一个更优雅的解决方案:
.calendar { display: grid; grid-template-columns: repeat(7, 1fr);}
repeat 函数将为我们完成复制/粘贴。我们说我们需要 7 列,每一列的宽度都是 1fr。
默认情况下,CSS 网格算法会将每个子元素分配到第一个未占用的网格单元格,很像一个熟练工人在地板上铺瓷砖一样。
不过最棒的是:我们可以把项目分配到我们想要的任何单元格里!孩子们甚至可以在多行/多列中展开。
这是一个展示此工作原理的交互式演示。点击/按住并拖动以将孩子放在网格中:
.parent { display: grid; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(4, 1fr);}.child { grid-row: 2; grid-column: 2;}
grid-row 和 grid-column 属性允许我们指定网格子元素应占据哪个轨道。
如果我们希望孩子占用单独一行或列,我们可以通过其编号来指定它。grid-column: 3 会将孩子设置在第三列中。
网格子组件也可以跨多行/列拉伸。其语法是使用斜杠来定义开始和结束:
.child { grid-column: 1 / 4;}
乍一看,这看起来像一个分数 ¼。然而,在 CSS 中,斜杠字符不用于除法,它用于分隔值组。在这种情况下,它允许我们在一个声明中设置开始和结束列。
这是一种简写,它相当于:
.child { grid-column-start: 1; grid-column-end: 4;}
这里有一个狡猾的陷阱:我们提供的值是基于列的“线”而不是列的“索引”。
从一张图中,最容易理解这个陷阱:
.child { grid-row: 2 / 4; grid-column: 1 / 4;}
虽然令人费解,但一个 4 列的网格实际上有 5 条列线。当我们将一个子元素分配到网格时,我们使用这些线来锚定它们。如果我们想要子元素跨越前 3 列,它需要从第 1 条线开始,并在第 4 条线上结束。
好的,现在要谈谈 CSS Grid 最酷的部分之一。
假设我们要建立此布局:
根据我们目前已经学到的东西,我们可以构建如下这种结构:
.grid { display: grid; grid-template-columns: 2fr 5fr; grid-template-rows: 50px 1fr;}.sidebar { grid-column: 1; grid-row: 1 / 3;}header { grid-column: 2; grid-row: 1;}main { grid-column: 2; grid-row: 2;}
这种方法可行,但有一种更符合人体工学的方法可以用:网格区域。
看看它长什么样:
.parent { display: grid; grid-template-columns: 2fr 5fr; grid-template-rows: 50px 1fr; grid-template-areas: 'sidebar header' 'sidebar main';}.child { grid-area: sidebar;}
一如既往,我们使用 grid-template-columns 和 grid-template-rows 定义网格结构。然后,我们有了这个奇怪的声明:
.parent { grid-template-areas: 'sidebar header' 'sidebar main';}
这原理如下:我们正在画我们想要创建的表格,有点像我们制作 ASCII 艺术一样。每一行表示一行,每个单词都是我们给表格特定部分起的名字。明白它为何从视觉上看起来像表格了吗?
于是,我们不再为子元素分配 grid-column 和 grid-row,而是为其分配 grid-area!
当我们希望某个区域跨越多行或多列时,可以在模板中重复此区域的名称。在此示例中,“sidebar”区域跨越两行,因此我们为第一列中的两个单元格都编写了“sidebar”。
应该使用区块还是行/列呢?在构建像这样明确的布局时,我非常喜欢使用区块。这可以让我为我的网格分配赋予语义含义,而不是使用难以理解的行/列编号。话虽如此,只有当网格有固定数量的行和列时,区块才能发挥最佳作用。grid-column 和 grid-row 可以用于隐式网格。
网格分配存在一个很大的问题:标签顺序仍将基于 DOM 位置,而非网格位置。
我们用一个例子来说明一下,在这个 playground 中,我准备了一组按钮,并用 CSS Grid 进行布局。
<div class="wrapper"> <button class="btn one"> One </button> <button class="btn four"> Four </button> <button class="btn six"> Six </button> <button class="btn two"> Two </button> <button class="btn five"> Five </button> <button class="btn three"> Three </button></div>
按钮似乎有顺序。从左向右、从上到下阅读,我们从一到六。
如果你有带键盘的设备,请尝试通过这些按钮来切换。 您可以先点击左上角的第一个按钮(“一”),然后连续按 Tab 键,逐个切换按钮。
您应该看到类似这样的内容:
用户看来,焦点轮廓没有任何道理地跳跃到页面的各个部分。发生这种情况是因为按钮的焦点基于它们在 DOM 中的顺序。
为了修复这个问题,我们应该在 DOM 中重新排列网格的子元素,以便它们与可视顺序匹配,这样我就可以从左到右,从上至下进行制表键切换。
在迄今为止的所有示例中,我们的列和行都延伸来填充整个网格容器。但这种情况并非一定要发生!
例如,我们定义了两个 90px 宽的列。只要网格父级大于 180px,那么就会在末端产生一些空白:
通过 justify-content 属性,我们可以控制列分布:
如果您熟悉 Flexbox 布局算法,那么这听起来可能很熟悉。CSS Grid 基于 Flexbox 首次引入的对齐属性,并将这些属性发挥到了极致。
最大的区别是,我们要调整的是 列,而不是元素本身。实质上,justify-content 让我们可以整理网格的隔间,以我们希望的方式在网格中分布。
如果我们想在列内对齐项目本身,我们可以使用 justify-items 属性:
当我们将一个 DOM 节点放入网格父元素中时,默认行为是使其跨越该列的整个宽度,就像流布局中的 <div> 会水平拉伸以填充其容器一样。但是使用 justify-items 可以调整这种行为。
这很有用,因为它允许我们摆脱列的刚性对称。当我们把 justify-items 设置为 stretch 以外的其他值时,子项将缩小至由其内容确定的默认宽度。结果,同一列中的项目可以具有不同的宽度。
我们可以使用 justify-self 属性来控制一个特定的网格子项的对齐方式:
与用于设置网格父级并控制所有网格子元素对齐方式的 justify-items 不同,justify-self 是在子元素上设置的。我们可以将 justify-items 看作在所有网格子元素上设置 justify-self 的默认值的方式。
迄今为止,我们一直在谈论如何在水平方向上对齐内容。CSS 网格布局提供了一组额外的属性,用以在垂直方向上对齐内容:
align-content与justify-content类似,但它影响的是行而不是列。类似地,align-items与justify-items类似,但它处理的是网格区域内项目_垂直_对齐方式,而不是水平对齐方式。
更进一步分解为:
最后,除了justify-self,我们还有align-self。该属性控制单元格内单个网格项的垂直位置。
还有一件事我必须给你演示一下。这是我喜欢的小技巧之一,用 CSS Grid 实现的。
使用仅有的两个 CSS 属性,我们可以让子元素在容器中居中,无论水平还是垂直方向:
.parent { display: grid; place-content: center;}
place-content 是一个简写形式属性。它等同于以下代码:
.parent { justify-content: center; align-content: center;}
正如我们已经了解的,justify-content 控制了列的位置。align-content 控制了行的位置。在这种情况下,我们有一个具有一个子项的隐式网格,因此我们最终得到一个 1×1 网格。place-content: center 将行和列都推到中心位置。
在现代 CSS 中有多种方法可以使 div 居中,但是这是我知道的唯一一种只需要两个 CSS 声明的方法!
本教程涵盖了 CSS 网格布局算法的一些基本内容,但老实说,我们还没有谈论过 很多东西!
如果你觉得这篇博文有帮助,你可能会对这个我精心制作的学习资源感兴趣,它非常深入。它叫《 CSS for JavaScript Developers.》。
本课程使用了与我的博客相同的技术,因此其充满了交互式说明。但也包含一些小视频、练习题、灵感源自现实世界的项目,甚至还有一些小游戏。
如果您觉得此博客文章有帮助,您会喜欢这门课程。它遵循类似的方法,但适用于整个 CSS 语言,并通过动手练习来确保您确实在发展新的技能。
专为使用 React/Angular/Vue 等 JS 框架的人打造。80% 的课程将教授 CSS 基础,我们还会学习如何将这些基础知识集成到现代 JS 应用程序中,如何构建 CSS 等。
如果你在学习 CSS 的时候遇到困难,我希望你能试试它。尤其在你已经熟悉 HTML 和 JS 的情况下,掌握 CSS 是能够彻底改变游戏规则的。当你掌握了三者之后,你便可以更容易地进入状态,真正地享受 Web 应用程序的开发。
希望这个指南对您有用。❤️
2023年11月22日