如何使用 CSS Grid 实现粘性头部和底部

Avatar of Adam Rackis
Adam Rackis

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

CSS Grid 是一组旨在简化布局的属性集合。就像任何事物一样,它也存在一定的学习曲线,但一旦掌握了 Grid,你会发现它真的 很有趣。它在处理头部和底部方面表现出色。通过稍微调整我们的思维方式,我们可以实现像固定一样行为的头部和底部,或者实现 “粘性”效果(不是 position: sticky,而是即使内容不足以将其推到那里,也始终位于屏幕底部的底部,并且随着内容的增加而被向下推的那种底部)。

希望这能激发您对现代布局的进一步兴趣,如果确实如此,我强烈推荐 Rachel Andrew 的书籍 The New CSS Layout:它涵盖了两种主要的现代布局技术,gridflexbox

我们将要实现什么

让我们实现一个相当经典的 HTML 布局,它包含头部、主要内容和底部。

我们将创建一个真正的固定底部,它始终位于视口底部,主要内容根据需要在其中滚动,然后稍后更新底部,使其成为更传统的粘性底部,即使主要内容较小,它也从视口底部开始,但根据需要会被向下推。此外,为了扩大我们对 grid 的了解,让我们设计我们的主要内容容器,使其可以跨越视口的整个宽度,或者占用中间的一条居中条带。

固定底部稍微有些不同寻常。底部通常设计为视口底部开始,并根据需要被主要内容向下推。但持久性底部并非闻所未闻。Charles Schwab 在其 主页 上使用了这种方式。无论哪种方式,实现它都将很有趣!

但在我们继续之前,不妨实际查看 Charles Schwab 网站上实现的固定底部。不出所料,它使用了固定定位,这意味着它具有硬编码的大小。事实上,如果我们打开 DevTools,我们会立即看到这一点

body #qq0 {
  border-top: 4px solid #133568;
  background-color: #eee;
  left: 0;
  right: 0;
  bottom: 0;
  height: 40px!important;
}

不仅如此,还要确保主要内容不会隐藏在该固定底部后面,它通过设置硬编码填充(包括 <footer> 元素底部的 15px)、边距(包括底部 <ul> 中的 20px)甚至换行来实现。

让我们尝试在没有任何这些限制的情况下实现这一点。

我们的基础样式

让我们勾勒出一个最基本的 UI 来帮助我们开始,然后增强我们的网格以匹配我们的目标。下面有一个 CodeSandbox,以及用于后续步骤的额外 Sandbox,这些步骤将帮助我们获得最终结果。

首先,让我们进行一些准备工作。我们将确保我们正在使用视口的整个高度,因此当我们添加网格时,可以轻松地将底部放置在底部(并保持在那里)。文档的 <body> 内部只有一个元素具有 #app 的 ID,它将包含 <header<main><footer> 元素。

body {
  margin: 0; /* prevents scrollbars */
}


#app {
  height: 100vh;
}

接下来,让我们设置我们的头部、主体和底部部分,以及它们将位于其中的网格。需要明确的是,这不会立即按照我们想要的方式工作。这仅仅是为了让我们开始,并建立一个基础。

body {
  margin: 0;
}


#app {
  height: 100vh;
  
  /* grid container settings */
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: auto 1fr auto;
  grid-template-areas: 
    'header'
    'main'
    'footer';
}


#app > header {
  grid-area: header;
}


#app > main {
  grid-area: main;
  padding: 15px 5px 10px 5px;
}


#app > footer {
  grid-area: footer;
}

我们创建了一个简单的单列布局,宽度为 1fr。如果您不了解 1fr,它基本上表示“占用剩余空间”,在本例中,它是网格容器 #app 的整个宽度。

我们还定义了三行

#app {
  /* etc. */
  grid-template-rows: auto 1fr auto;
  /* etc. */
}

第一行和第三行,分别是我们的头部和底部,大小为 auto,这意味着它们将占用所需的空间。换句话说:无需硬编码大小!这是一个非常重要的细节,也是我们使用 CSS Grid 获益的完美示例。

中间一行是我们放置内容的地方。我们为它分配了 1fr 的大小,这再次意味着它占用其他两行剩余的所有空间。如果您想知道为什么我们也不将其设置为 auto,那是因为整个网格跨越视口的整个高度,所以我们需要一个部分来增长并填充任何未使用的空间。请注意,我们没有,而且将来也不需要任何固定高度、边距、填充 - 甚至换行! - 来将内容推到位。使用 grid 时,生活就是这样美好!

我们来尝试一些内容吧?

您会在 Sandbox 中注意到我使用了 React 来构建此演示,但由于这不是一篇关于 React 的文章,所以我不会过多赘述这些细节;React 与本文中的任何 CSS Grid 工作都绝对没有关系。我只是用它作为在不同代码块之间轻松导航的一种方法。如果您不喜欢 React,没关系:希望您可以在本文中忽略它。

我们有 HeaderMainFooter 组件,它们分别呈现预期的 <header><main><footer> 元素。当然,这一切都位于我们的 #app 容器内。是的,从理论上讲,#app 应该是一个 <article> 元素,从语义上讲,但这对我来说一直看起来很奇怪。我只是想传达这些细节,以便我们在前进的过程中都在同一页面上。

对于实际内容,我创建了“账单”和“设置”部分,您可以在头部之间导航。它们都呈现虚假的静态内容,并且仅用于展示我们的布局。设置部分将是我们放在页面居中条带上的内容,“账单”部分将是跨越我们整个页面的内容。

这是我们到目前为止的 Sandbox

“账单”部分看起来不错,但“设置”部分将我们的底部推出了屏幕。不仅如此,如果我们滚动,整个页面都会滚动,导致我们丢失头部。在某些情况下,这可能是理想的,但我们希望头部和底部都保持可见,因此让我们修复它。

当我们最初设置网格时,我们为它设置了 100vh 的高度,也就是视口的整个高度。然后,我们将头部和底部的行设置为 auto 高度,并将主体的高度设置为 1fr 以占用剩余的空间。不幸的是,当内容超出可用空间时,它会扩展到视口边界之外,将我们的底部向下推并超出视野。

这里的修复很简单:添加 overflow: auto 将导致我们的 <main> 元素滚动,同时保持我们的 <header><footer> 元素在适当位置。

#app > main {
  grid-area: main;
  overflow: auto;
  padding: 15px 5px 10px 5px;
}

这是将此应用于实践的 更新后的演示

可调整宽度的主要部分

我们希望我们的 <main> 元素能够跨越视口的整个宽度,或者居中在一个 600px 的空间中。您可能会认为我们可以简单地将 <main> 设置为 600px 的固定宽度,并在两侧使用自动边距。但由于这是一篇关于网格的文章,所以让我们使用更多网格。(此外,正如我们稍后将看到的,固定宽度也无法正常工作)。

为了实现我们的居中 600px 元素,我们实际上将 <main> 元素设置为网格容器。没错,网格嵌套在网格中!嵌套网格 是一种完全合法的方法,并且在未来当 subgrid 在所有浏览器中正式得到支持时,会变得更容易。在这种情况下,我们将 <main> 设置为具有三个列轨道的网格,分别为 1fr 600px 1fr,或者简单地说,中间为 600px,剩余的空间平均分配到两侧。

#app > main {
  display: grid;
  grid-template-rows: 1fr;
  grid-template-columns: 1fr 600px 1fr;
}

现在让我们在网格中定位内容。我们的不同模块都在一个 <section> 子元素中呈现。假设默认情况下,内容将占用中间部分,除非它具有 .full 类,在这种情况下,它将跨越整个网格宽度。我们不会在这里使用命名区域,而是指定 [row-start] / [col-start] / [row-end] / [col-end] 形式的精确网格坐标

#app > section {
  grid-area: 1 / 2 / 1 / 3;
}


#app > section.full {
  grid-area: 1 / 1 / 1 / 4
}

鉴于只有三列,您可能会惊讶地看到 col-end 值为 4。这是因为列和行值是列和行网格线。绘制三列需要四条网格线。

我们的 <section> 将始终位于第一行,这是唯一一行。默认情况下,它将跨越列线 23,即中间列,除非该部分具有 full 类,在这种情况下,它将跨越列线 14,即所有三列。

这段代码的更新后的演示在这里。根据你的 CodeSandbox 布局,它可能看起来不错,但仍然存在问题。如果将显示尺寸缩小到小于 600px,内容会被突然截断。我们并不希望中间有一个固定的 600px 宽度。我们想要的是最多600px 的宽度。事实证明,网格正好有我们需要的工具:minmax() 函数。我们指定一个最小宽度和一个最大宽度,网格将计算一个介于此范围内的值。这就是我们防止内容撑破网格的方式。

我们只需要将那个 600px 值替换为 minmax(0, 600px) 即可。

main {
  display: grid;
  grid-template-rows: 1fr;
  grid-template-columns: 1fr minmax(0, 600px) 1fr;
}

这是完成代码的演示

之前,我们决定防止页脚被推出屏幕,并通过将 <main> 元素的 overflow 属性设置为 auto 来实现这一点。

但是,正如我们简要提到的,这可能是一个理想的效果。实际上,这更像是一个经典的“粘性”页脚,它解决了那个恼人的问题,并在内容非常短时将页脚放置在视口的底部边缘。

嘿,回到底部!

我们如何保留所有现有工作,但允许页脚被向下推,而不是将其固定到底部以持续显示?

现在我们的内容在一个具有以下 HTML 结构的网格中

<div id="app">
  <header />
  <main>
    <section />
  </main>
  <footer />
</div>

…其中 <main> 是嵌套在 #app 网格容器内的网格容器,它包含一行和三列,我们用它们来定位模块的内容,这些内容位于 <section> 标签中。

 让我们将其更改为以下内容

<div id="app">
  <header />
  <main>
    <section />
    <footer />
  </main>
</div>

…并将 <footer> 整合到 <main> 元素的网格中。我们将首先更新父级 #app 网格,使其现在包含两行而不是三行

#app {
  /* same as before */


  grid-template-columns: 1fr;
  grid-template-rows: auto 1fr;
  grid-template-areas: 
    'header'
    'main';
}

只有两行,一行用于标题,另一行用于所有其他内容。现在让我们更新 <main> 元素内部的网格

#app > main {
  display: grid;
  grid-template-rows: 1fr auto;
  grid-template-columns: 1fr minmax(0, 600px) 1fr;
}

我们引入了一行新的自动大小的行。这意味着我们现在有一行 1fr 用于我们的内容,它包含我们的 <section>,以及一行 auto 用于页脚。

现在我们将 <footer> 定位到这个网格中,而不是直接定位到 #app

#app > footer {
  grid-area: 2 / 1 / 3 / 4;
}

由于 <main> 是具有滚动功能的元素,并且由于此元素现在包含我们的页脚,因此我们实现了所需的粘性页脚!这样,如果 <main> 的内容超出视口,则整个内容将滚动,并且该滚动内容现在将包括我们的页脚,它将位于屏幕的最底部,正如我们预期的那样。

这是更新后的演示。请注意,如果可能,页脚将位于屏幕底部;否则,它将根据需要滚动。

我做了一些其他的小改动,比如这里和那里的填充调整;我们不能在 <main> 上有左右填充,因为 <footer> 将不再是边缘到边缘的。

在最终编辑过程中,我还对 <section> 元素(我们在其中启用了可调整宽度内容的元素)进行了最后一分钟的调整。具体来说,我将其显示设置为 flex,其宽度设置为 100%,并且其直接后代设置为 overflow: auto。这样做是为了使 <section> 元素的内容可以在其自身内部水平滚动,如果它超出了我们的网格列边界,但不允许任何垂直滚动。

如果没有此更改,我们所做的工作将相当于我们之前介绍的固定页脚方法。使 section> 成为一个弹性容器会强制其直接子元素(包含内容的 <div>)占据所有可用的垂直空间。当然,将该子 div 设置为 overflow: auto 可以启用滚动。如果你想知道为什么我不只是将 section 的 overflow-x 设置为 auto,并将 overflow-y 设置为 visible,那么事实证明这是不可能的

结语

我们在本文中没有做任何革命性的事情,当然也没有任何在 CSS Grid 之前无法完成的事情。我们的固定宽度 <main> 容器可以是一个块级元素,其 max-width 值为 600px,并且左右边距为自动。我们的固定页脚可以使用 position: fixed 制作(只需确保主要内容不会与它重叠)。当然,还有各种方法可以获得更传统的“粘性页脚”。

但是 CSS Grid 提供了一种单一、统一的布局机制来完成所有这些操作,并且它使用起来很有趣——说真的,很有趣。事实上,将页脚从固定更改为粘性的想法一开始甚至不是我计划的。我在最后一刻加上了它,因为我认为如果没有它,这篇文章会显得有点太轻了。实现起来非常简单,基本上就是移动网格行,就像拼乐高积木一样。再说一次,这些 UI 非常简单。想象一下,当使用更具雄心的设计时,网格将如何闪耀光芒!