CSS Grid 是一组旨在简化布局的属性集合。就像任何事物一样,它也存在一定的学习曲线,但一旦掌握了 Grid,你会发现它真的 很有趣。它在处理头部和底部方面表现出色。通过稍微调整我们的思维方式,我们可以实现像固定一样行为的头部和底部,或者实现 “粘性”效果(不是 position: sticky
,而是即使内容不足以将其推到那里,也始终位于屏幕底部的底部,并且随着内容的增加而被向下推的那种底部)。
希望这能激发您对现代布局的进一步兴趣,如果确实如此,我强烈推荐 Rachel Andrew 的书籍 The New CSS Layout:它涵盖了两种主要的现代布局技术,grid 和 flexbox。
我们将要实现什么
让我们实现一个相当经典的 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,没关系:希望您可以在本文中忽略它。
我们有 Header
、Main
和 Footer
组件,它们分别呈现预期的 <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>
将始终位于第一行,这是唯一一行。默认情况下,它将跨越列线 2
到 3
,即中间列,除非该部分具有 full
类,在这种情况下,它将跨越列线 1
到 4
,即所有三列。
这段代码的更新后的演示在这里。根据你的 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 非常简单。想象一下,当使用更具雄心的设计时,网格将如何闪耀光芒!
总是回来阅读像这样的文章
很棒的文章,Adam。非常感谢。但我对这部分感到困惑
#app > footer {
grid-area: 2 / 1 / 2 / 4;
}
它不应该
#app > footer {
grid-area: 2 / 1 / 3 / 4;
}
吗?抱歉,我是 CSS 新手
嗨!实际上,我不太确定,但我正在检查。看起来你可能是对的,但我的方法似乎也起作用。我正在检查我使用的方法是否符合规范且正确,或者仅仅是某些碰巧有效的未定义实现细节。即使是前者,我也考虑调整它以使其更清晰。
感谢你指出这一点。
它应该是 grid-area: 2 / 1 / 3 / 4; 否则你试图在同一行上开始和结束。由于你总是必须跨越至少一个轨道,所以它正在起作用,因为它被忽略了,因此被视为跨度 1。这将是 DevTools 值得关注的事情,即使我使用详细语法,它们似乎也不会关注。
规范中的示例 35 给出了跨越相同开始和结束行的示例,将其列为错误:https://www.w3.org/TR/css-grid-1/#grid-placement-span-int
粘性页脚始终困扰我的一点是在移动设备上的行为。例如,此演示在 Android 上的 Chrome 或 FF 上效果不佳。根据我的经验,iOS 浏览器甚至更难正常工作。我认为,只要我们有应用程序抽屉和地址栏会在用户滚动视口时消失/重新出现,这些布局就会一直受到影响!
这确实是一个很好的观点。这令人沮丧,我可能会添加一条注释来指出这一点。谢谢!
实际上,这种情况有解决办法。你可以使用 100dvh,它代表 100 设备视口高度,它将计算可用屏幕的实际高度,不包括地址栏或类似元素。除了 IE 和 Opera 之外,浏览器的支持正在变得越来越好 :)
使用 dvh 时也使用 vh
height: 100vh;
height: 100dvh;
如果浏览器不理解 dvh,则 vh 版本将生效。
为了更深入地理解,dvh 会随着键盘在移动设备上出现和降低而变化。
有趣的文章,谢谢!
我想知道你是否可以超越你的第二个示例(如果可能的话),并在页脚下方添加内容,这些内容会被主要内容推离屏幕。
所以你会有一个粘性页脚,但也会有在其下方不粘性的内容。
你可以将其视为一个可折叠的页脚,只有当主要区域向上滚动足够远以显示它时,才会显示其子内容。
一个与同一个滚动容器同在的页脚-页脚 :)
嗨,Adam,
注意
<header /><main /><footer />
与<header /><main>...<footer/></main>
的语义含义不同。对我来说,这是一种不好的开发实践!是啊,问题就在这儿。我仔细阅读了这篇文章,完全打算去实现,然后我看到了这个不算解决方案的东西。如果你不介意让屏幕阅读器用户感到困惑,你就可以得到你想要的布局!Adam,在文章开头就说明你的解决方案将页脚嵌套在 main 中,这样那些必须实现正确语义的读者就可以跳过了。
我曾经尝试过这个技巧,但后来放弃了,因为它会破坏键盘滚动:上/下/page up/page down/空格键用于向下翻页。无论多么酷,如果一个想法破坏了可访问性,它就不值得。
键盘滚动在这个方案下工作良好。
看起来非常有趣。我希望你在文章中添加一些 HTML 代码,而不是(仅)codesandbox 链接。这些链接迫使我一个标签一个标签地跳转。
子网格链接(mozilla 网站)无法使用,新的链接
https://mdn.org.cn/en-US/docs/Web/CSS/CSS_Grid_Layout/Subgrid
谢谢 Adam,你的文章帮助我解决了问题。
我尝试提取你博客的要点:https://dev.to/xaverfleer/how-i-use-css-grid-for-sticky-headers-2ong
好奇你对此有何看法。
滚动溢出内容,无需轮询、计算和设置高度?
哇!
一个简单、最少、自然的(非 JS)跨浏览器解决方案,可以解决日常的烦恼。
谢谢!
#app {
height: 100vh;
}
应该是
#app {
min-height: 100vh;
}
否则在某些情况下,即使“position: sticky”,标题也会被滚动。
类似情况
https://stackoverflow.com/questions/30227863/sticky-position-header-scrolled-away-after-a-while