基于CSS网格布局的全屏布局实现

本文翻译自《Full-Bleed Layout Using CSS Grid》。

过去,每个人都力图创建一种黄金标准的网站布局:圣杯布局。但众所周知,这种布局很难做到正确。

这看起来似乎不是那么棘手,对吧?但那是flex布局出现之前的时代;我们用于这项工作的工具是表格和浮动布局,但两者都不能真正胜任这项任务。这在技术上是可行的,但需要一些额外技巧。

一旦flex获得主流浏览器支持,这种布局就从“圣杯”变成了“喷泉饮料”;它无处不在,因为它提供了出色的用户体验,并且所有开发人员都可以使用。

随着网络的发展,我发现了一种新的理想的布局。它提供了极好的用户体验,尤其是对于新闻文章或文档等长篇文本内容。但是,就像它的前辈一样,它很难实现;大多数实现都需要晦涩难懂的黑客手段或违反直觉的技巧。

我最近发现了一个使用CSS Grid的优雅解决方案。在这篇文章中,我们将了解它的工作原理!

问题所在

你曾经尝试过在超大屏幕上阅读维基百科吗?它看起来像这样:

在body元素上设置id属性的值作为CSS命名空间

比如说有2个页面index.html和about.html,共用同一份style.css,那么怎么区分index.html和about.html的CSS属性不致于混淆呢?

其中一种好用的解决方法是,给index.html的body元素设置id=”index”属性,给about.html的body元素设置id=”about”属性。在style.css文件里,分别使用#index和#about作为包含选择器的开头,分别用来限定index.html和about.html的CSS属性。例如:

/* index.html的CSS */
#index div {
    /* …. */
}

/* about.html的CSS */
#about  div {
    /* …. */
}

CSS网格Grid布局交互式指南

本文翻译自《An Interactive Guide to CSS Grid》。

CSS Grid是CSS语言中最令人惊叹的部分之一。它为我们提供了大量的新工具,可以用来创建复杂又流畅的布局。

CSS Grid非常复杂。我花了很长时间才真正适应它!

在本教程中,我将分享在我使用CSS Grid的过程中,我经历的一些精彩的时刻。你将学习这种布局模式的基础知识,并了解如何用它做一些很酷的事情。

浏览器支持情况?

CSS Grid是构建CSS布局的最现代的工具,但它并不是“前沿”。自2017年以来,所有主流浏览器都支持它!

根据caniuse的数据,97.8%的用户支持CSS Grid。Flexbox仅超过它约0.5%!

心智模型

CSS由几种不同的布局算法组成,每种算法都是为不同类型的用户界面设计的。默认布局算法Flow layout是为数字文档设计的。表格布局是为表格数据而设计的。Flexbox布局用于沿单个轴分配子条目。

CSS Grid是最新、最伟大的布局算法。它非常强大:我们可以用它来构建复杂的布局,这些布局可以根据许多约束流畅地适应。

在我看来,CSS Grid最不寻常的部分是网格结构行和列完全在CSS中定义

(译者注:案例请看原文)

使用CSS Grid,单个DOM节点被细分为行和列。在本教程中,我们用虚线突出显示行/列,但实际上它们是不可见的。

这太奇怪了!在所有其他布局模式中,创建这样的隔间的唯一方法是添加更多的DOM节点。例如,在Table布局中,每一行都使用<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 Grid允许我们完全从CSS内部管理布局。我们可以随心所欲地分割容器,创建我们的网格子节点可以用作锚点的隔间。

网格容器使用流式布局(Grid flow)

我们使用display属性选择网格布局模式:

.wrapper {
  display: grid;
}

默认情况下,CSS Grid使用单列,并将根据子条目的数量根据需要创建一行或多行。这被称为隐式网格,因为我们还没有明确定义任何结构。

这就是它在默认情况下如何工作的:

(译者注:案例请看原文)

隐式网格是动态的;将根据子条目的数量添加或删除一行或多行。每个子条目都有自己的一行。

默认情况下,网格的容器元素的高度由其子条目的数量决定。它动态地增长和收缩。有趣的是,这甚至不是一个“CSS网格”的特性;网格的容器元素仍在使用文档流布局(Flow layout),Flow布局中的块状元素(display:block)默认会动态地垂直增长以包含其内容。只有里面的子元素使用网格布局(Grid layout)来排列。

但是,如果我们给网格的容器元素一个固定的高度呢?在这种情况下,里面的行将平均分配它的高度,即每一行都有相同的高度:

(译者注:案例请看原文)

网格构建(Grid Construction)

默认情况下,CSS Grid将创建单列布局。我们可以使用grid-template-columns属性指定列:

(译者注:案例请看原文)

为清晰起见,添加了虚线

在上面的案例以及在整个教程中,我使用虚线来显示列和行之间的划分。在CSS网格布局中,这些线是不可见的,也无法使其可见。在本文章,这些虚线只做演示用。

通过grid-template-columns: 25% 75%;属性,我告诉CSS网格布局算法将元素分为两列。

列可以使用任何有效的CSS长度值来定义,包括像素、rems、视口单位等。此外,我们还可以使用一个新的单位——fr单位:

(译者注:案例请看原文)

fr代表“分数”。在这个例子中,我们说第一列消耗1单位的空间,而第二列消耗3单位的空间。这意味着总共有4个单位的空间,这成为分母。第一列消耗了1/4的可用空间,而第二列消耗了3/4。

fr单位为CSS网格带来了类似于Flexbox样式的灵活性。百分比和长度值创建了硬约束,而fr列可以根据需要自由增长和收缩,以包含其内容。

尝试缩小此容器以查看fr单位和百分比单位之间的差异:

(译者注:案例请看原文)

在上述案例中,我们的第一列有一张可爱的幽灵图片,它的宽度被设置为55px。但是,如果列太小而无法容纳它呢?

  • 基于百分比单位的列是刚性的,因此我们的图像会溢出,溢出到列外。
  • 基于fr单位的列是灵活的,因此列不会缩小到其最小内容的尺寸以下,即使这意味着会打破比例。

更准确地说:fr单位分配额外的空间。首先,列宽将根据列的内容计算。如果容器还有剩余空间,将根据fr单位的值进行按比例分配。这与我在《Flexbox交互式指南》中讨论的flex-grow属性的功能非常相似。

总的来说,这种灵活性是一件好事。百分比单位太严格了。

我们可以在gap中看到一个完美的例子。gap是一个神奇的CSS属性,它在网格中的所有列和行之间添加了固定数量的空间。

看看当我们在百分比单位和fr单位之间切换时会发生什么:

(译者注:案例请看原文)

请注意,当使用基于百分比单位的列时,内容是如何溢出到网格容器之外的。这是因为百分比单位是使用网格容器的总面积来计算的。这两列占用了网格容器内容区域的100%,并且不允许收缩。当我们添加16px的间隙时,这两列别无选择,只能溢出网格容器。

相比之下,fr单位是根据网格容器的额外空间计算的。在这种情况下,由于设置了间隙gap:16px;,网格容器的额外的空间减少了16px。CSS网格算法在两个网格列之间按比例分配剩余空间。

gapgrid-gap

当CSS Grid首次引入时,grid-gap属性用于在列和行之间添加空间。然而,很快,社区意识到,在Flexbox中拥有这个功能也会很棒。因此,该属性被赋予了一个更通用的名称:gap。

如今,grid-gap已被标记为弃用,浏览器将其取别名为gap。这两个属性做完全相同的事情。它们都有几乎相同的浏览器支持,约96%

因此,我建议使用gap而不是grid-gap,无论你是使用Flexbox还是CSS grid。但也不强制转换现有的grid-gap声明。

隐式的行和显式的行

如果我们在两列网格中添加两个以上的子元素,会发生什么?

好吧,让我们试试:

(译者注:案例请看原文)

有趣!我们的网格获得了第二行。网格算法希望确保每个子元素都有自己的网格单元。它将根据需要生成新行以实现这一目标。这在我们有可变数量的子元素(例如照片网格)并且我们希望网格能自动扩展的情况下很方便。

不过,在其他情况下,我们希望显式定义一行或多行,以创建特定的布局。我们可以使用grid-template-rows属性来实现这一点:

(译者注:案例请看原文)

通过定义grid-template-rows和grid-template-columns,我们创建了一个显式的网格。这非常适合构建页面布局,比如在本教程的顶部的“圣杯”布局。

重复助手

假设我们正在构建一个日历:

对于这种场景,CSS Grid是一个很好的工具。我们可以将其构造为7列网格,每列占用1个单位的空间:

.calendar {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
}

这是可行的,但要写出每一个1fr有点烦人。想象一下,如果我们有50列!

幸运的是,有一个更好的方法来解决这个问题:

.calendar {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
}

repeat函数将为我们进行复制/粘贴。我们只需要说我们想要7列,每列宽1fr。

以下是这个日历的完整代码:

(译者注:案例请看原文)

分配子项

默认情况下,CSS网格布局算法会将每个子元素分配到第一个未被占用的网格单元,就像商人在浴室地板上铺设瓷砖一样。

不过,有一件很酷的事情:我们可以将我们的子元素分配给任何一个单元格!甚至可以跨越多行/多列。

这是一个交互式演示,展示了CSS网格布局算法是如何工作的。单击/按下或拖动以将子元素放置在网格中:

(译者注:案例请看原文)

grid-row和grid-column 属性允许我们指定网格布局的子元素应占用的网格空间。

如果我们想让子对象占据一行或一列,我们可以通过一个编号来指定它。grid-column:3将设置子元素坐在第三列。

网格子元素还可以拉伸并跨越多行或多列。此语法使用斜线来描述开始和结束:

.child {
  grid-column: 1 / 4;
}

乍一看,这看起来像一个分数1/4。然而,在CSS中,斜线字符不用于分割,而是用于分隔一组值。在这种情况下,它允许我们在单个声明中设置开始列和结束列。

这实质上是对此的简写:

.child {
  grid-column-start: 1;
  grid-column-end: 4;
}

这里有一个狡猾的陷阱:我们提供的数字是基于列线,而不是列索引。

用图表最容易理解这个陷阱:

容易令人困惑的是,一个4列的网格实际上有5条列线。当我们为网格分配一个子元素时,我们使用这些线来锚定它们。如果我们想让我们的子元素跨越前3列,它需要从第1个列线开始,到第4个列线结束。

负数的列线

在像英语这样的从左到右读写的语言中,我们从左到右数列。然而,对于负的列号或行号,我们也可以从右向左反向计数。

.child {
  /* 表示从右往左数的第2条列线 */
  grid-column: -2;
}

真正酷的是,我们可以混合使用正数和负数。看看这个:

请注意,即使我们根本没有更改网格列的分配,子元素也跨越了网格的整个宽度!

我们在这里说,我们的子元素应该跨越第一列到最后一列。无论有多少列,这个方便的声明都会按预期工作。

你可以在我的博客文章“使用CSS网格布局进行全出血布局”中看到这个技巧的一个实际用例。

网格区域(grid areas)

好了,是时候谈谈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;
}

这是可行的,但有一种更符合人体工程学的方法:使用网格区域(grid areas)相关属性。

下面是它的样子:

(译者注:案例请看原文)

与之前一样,我们使用grid-template-columns和grid-template-rows定义网格结构。但是,我们有一个奇怪的声明:

.parent {
  grid-template-areas:
    'sidebar header'
    'sidebar main';
}

这是如何工作的:我们正在绘制我们想要创建的网格,就像我们在制作ASCII文字艺术一样。每一行文字代表网格的一行,每个单词都是我们为网格的特定切片命名的。看看它在视觉上是如何排序的。

然后,我们不是用grid-column和grid-row来分配子元素,而是用grid-area来分配它!

当我们希望特定区域跨越多行或多列时,我们可以在模板中重复该区域的名称。在这个例子中,“sidebar”区域跨越了两行,即第一列中的两个单元格成为了“sidebar”区域。

我们应该使用grid-area还是grid-column和grid-row?当构建显式布局时,我真的很喜欢使用grid-area。它允许我为网格的分配赋予语义意义,而不是使用难以理解的行/列编号。也就是说,当网格具有固定数量的行和列时,使用grid-area效果最佳。而grid-column和grid-row对于隐式网格很有用。

注意键盘用户

当涉及到网格分配时,有一个很大的问题:选项卡的顺序仍将基于DOM位置,而不是网格位置

用一个例子来解释会更容易。在这个例子里,我设置了一组按钮,并用CSS Grid排列它们:

(译者注:案例请看原文)

当涉及到网格分配时,有一个很大的问题:选项卡的顺序仍将基于DOM位置,而不是网格位置

用一个例子来解释会更容易。在这个例子里,我设置了一组按钮,并用CSS Grid排列它们:

(译者注:案例请看原文)

在“结果”窗格上,按钮似乎按顺序排列。从左到右,从上到下阅读,从一到六。

如果你使用的是带键盘的设备,试着按一下这些按钮。你可以通过单击左上角的第一个按钮(“One”),然后按Tab键一次移动一个按钮。

你应该看到这样的效果:

(译者注:案例请看原文)

从用户的角度来看,焦点轮廓在页面上毫无韵律或理由地跳跃。这是因为按钮的焦点是基于它们在DOM中的顺序排序的

为了解决这个问题,我们应该重新排序DOM中的网格子元素,使其与视觉顺序相匹配,这样我就可以实现从左到右、从上到下按视觉顺序进行制表。

CSS属性reading-order

CSS工作组意识到这是CSS网格布局的一个缺陷。必须将网格子元素的DOM顺序与视觉顺序相匹配,这违背了可以将子元素随意分配给特定网格单元格的目的!

修复工作正在进行中。将来,CSS属性reading-order应该允许我们指示浏览器忽略DOM顺序,并按照子元素在屏幕上的显示顺序聚焦子元素。

不幸的是,截至2024年9月,此属性尚未在任何浏览器中实现。你可以在Rachel Andrew的帖子“解决CSS布局和源代码顺序断开连接的问题”中了解有关此属性的更多信息。

对齐

在我们目前为止看到的所有示例中,我们的列和行都会拉伸以填充整个网格容器。然而,事实并非如此!

例如,假设我们定义了两列,每列宽90px。只要网格容器的宽度大于180px,就会有一些额外空间:

我们可以使用justify-content属性来控制列的分布:

(译者注:案例请看原文)

如果你熟悉Flexbox布局算法,这可能会让你感觉似曾相识。CSS Grid建立在Flexbox首次引入的对齐属性的基础上,进一步发展了这些属性。

最大的区别在于,我们对齐的是列,而不是子元素本身。本质上,justify-content允许我们排列网格的各个部分,并按照我们的意愿在网格中分布它们。

如果我们想在列中对齐子元素本身,我们可以使用justify-items属性:

(译者注:案例请看原文)

当我们将DOM节点放入网格父容器时,默认行为是将宽度其拉伸充满整个列,就像Flow布局中的<div>被水平拉伸以填充其容器一样。但是,使用justify-items,我们可以调整该行为。

这很有用,因为它允许我们摆脱列的严格对称性。当我们将justify-items设置为拉伸以外的值时,子元素将缩小到其默认宽度,由其内容决定(译者注:适应其内容)。因此,同一列中的子元素可以具有不同的宽度。

我们甚至可以使用justify-self属性控制特定网格子元素的对齐方式:

(译者注:案例请看原文)

与justify-items不同,justify-items设置在网格容器上,并控制所有网格子元素的对齐方式,而justify-self设置在子元素上。我们可以将justify-items视为一种为所有网格子元素的justify-self属性设置默认值的方法。

对齐行

到目前为止,我们一直在讨论如何在水平方向上对齐内容。CSS Grid 提供了一组额外的属性来在垂直方向上对齐内容:

(译者注:案例请看原文)

align-content与justify-content类似,但它影响的是行而不是列。同样,align-items与justify-items类似,但它处理网格区域内的网格的垂直对齐,而不是水平对齐。

进一步细分:

  • justify — 处理列。
  • align — 处理行。
  • content — 处理网格结构。
  • items — 处理网格结构内的DOM节点(子元素)。

最后,除了justify-self,我们还有align-self。此属性控制单个网格子元素在其单元格内的垂直位置。

双线居中技巧

最后我想向你展示我最喜欢的CSS Grid小技巧之一。

仅使用两个CSS属性,我们就可以将子元素水平和垂直地置于容器内:

(译者注:案例请看原文)

place-content属性是以下CSS样式的语法糖:

.parent {
  justify-content: center;
  align-content: center;
}

正如我们所了解的,justify-content控制列的位置。align-content控制行的位置。在这种情况下,我们有一个带有单个子元素的隐式网格,因此我们最终得到一个 1×1 网格。place-content: center会将行和列都推到网格容器的中心。

在现代CSS中有很多方法可以将一个div居中,但以上是我所知道的唯一只需要两个CSS属性的方法!

CSS Global Initial Styles

Setting CSS global initial styles is mainly for ensuring compatibility across different browsers. Here’s an example:

/* Global Initial Styles */
html, body, div, span, h1, h2, h3, h4, h5, h6, a, em, img, p, dd, dl, dt, ul, li, ol, form, label, table, tr, td, input {
    margin: 0;
    padding: 0;
    color: #333;
}
p {
    font-size: 14px;
}
body {
    width: 100%;
    font-size: 12px;
    font-family: "Microsoft YaHei", Arial, Helvetica, serif;
    background-color: #f5f4f9;
}
a, a:link, a:visited {
    text-decoration: none;
}
a:hover {
    color: #e60012;
}
li {
    list-style: none;
}
img {
    border: none;
}
.clear {
    clear: both;
}

If you’re using CSS frameworks like Bootstrap, LayUI, or Tailwind CSS, they’ve already set these global initial styles for you, so you don’t need to set them yourself.

CSS全局初始样式

设置CSS全局初始样式,主要为了兼容不同浏览器。以下是一个示例:

/* 全局初始样式 */
html,body,div,span,h1,h2,h3,h4,h5,h6,a,em,img,p,dd,dl,dt,ul,li,ol,form,label,table,tr,td,input{
    margin: 0;
    padding: 0;
    color:#333;
}
p{
    font-size:14px;
}
body{
    width:100%;
    font-size: 12px;
    font-family: 微软雅黑,Arial,Helvetica, serif;
    background-color:#f5f4f9;
}
a, a:link, a:visited{
    text-decoration: none;
}
a:hover{
    color:#e60012;
}
li{
    list-style:none;
}
img{
    border:none;
}
.clear{
    clear:both;
}

如果你使用一些CSS框架,例如Bootstrap、LayUI、Tailwind CSS等,它们已经为你设置了CSS全局初始样式,无需自己设置了。

CSS样式单的基本使用

CSS(Cascading Style Sheet,级联样式单,也称为层叠样式单),主要用于控制页面外观,包括HTML元素的精确定位,网页的布局,字体的大小和颜色等。

在写作中,应该避免书写“CSS样式”、“CSS样式单”、“CSS样式文件”等短语,因为CSS本就包含“样式单”这个语义。

HTML5规范推荐用HTML标签控制文档结构,用CSS控制页面外观。

HTML5删除了传统的<font>、<big>、<strike>等专门控制页面外观的标签,推荐完全用CSS控制页面外观。


在HTML文档中使用CSS,有以下4种方式:
1.链接外部样式文件。这种方式将CSS彻底与HTML文档分离,一份CSS可以控制多份HTML文档。CSS使用<link>元素链接到HTML文档。

2.导入外部样式文件。这种方式与“链接外部样式文件”类似,只是使用@import来导入外部样式文件

3.使用内部样式定义。这种方式是在HTML文档头部(<head>元素内)使用<style>元素定义CSS,只控制当前HTML文档的样式。

4.使用行内样式。这种方式通过HTML元素的style属性,将CSS定义到具体的某个HTML元素,用于精确控制单个HTML元素的样式。