三行代码搭一个网格
在容器上设置 display: grid,再用 grid-template-columns 列出它的列。子元素按顺序填入单元格(在英文中从左到右),每当列被填满就开始新的一行:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>CSS grid</title>
<style>
body { font-family: system-ui, sans-serif; }
.grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr; /* three equal columns */
gap: 12px;
}
.grid div { background: #2563eb; color: white; padding: 16px; border-radius: 6px; }
</style>
</head>
<body>
<div class="grid">
<div>1</div><div>2</div><div>3</div>
<div>4</div><div>5</div><div>6</div>
<div>7</div>
</div>
</body>
</html>
七个元素、三列:两整行,第三行的第一列放元素 7。每列宽度相同,同一行的每个元素高度相同,所以边缘在两个方向上都对齐。这正是 grid 比 flexbox 多出来的能力。
Grid 在线演练
输入任意 grid-template-columns 值,或点击一个预设。读数显示浏览器算出的列宽(来自 getComputedStyle),以及这些元素占用了多少行:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Grid playground</title>
<style>
body { font-family: system-ui, sans-serif; font-size: 14px; }
.presets { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0; }
.presets button { font: 12px ui-monospace, monospace; padding: 4px 8px; border: 1px solid #111827; background: white; cursor: pointer; }
#grid { display: grid; gap: 8px; padding: 8px; background: #e5e7eb; }
#grid div { background: #16a34a; color: white; padding: 10px; text-align: center; }
pre { white-space: pre-wrap; background: #f3f4f6; padding: 8px; }
</style>
</head>
<body>
<label>grid-template-columns: <input id="cols" size="36" value="repeat(3, 1fr)"></label>
<div class="presets" id="presets"></div>
<div id="grid"></div>
<pre id="out"></pre>
<script>
const grid = document.getElementById('grid');
const input = document.getElementById('cols');
for (let i = 1; i <= 8; i++) grid.insertAdjacentHTML('beforeend', '<div>' + i + '</div>');
const update = () => {
grid.style.gridTemplateColumns = input.value;
const cols = getComputedStyle(grid).gridTemplateColumns;
const rows = getComputedStyle(grid).gridTemplateRows.split(' ').length;
document.getElementById('out').textContent =
'columns: ' + cols + '\n' + cols.split(' ').length + ' columns, ' + rows + ' rows';
};
['repeat(3, 1fr)', '1fr 2fr', '200px 1fr', 'auto 1fr auto', 'repeat(4, 100px)', 'repeat(auto-fill, minmax(120px, 1fr))'].forEach((v) => {
const b = document.createElement('button');
b.textContent = v;
b.addEventListener('click', () => { input.value = v; update(); });
document.getElementById('presets').append(b);
});
input.addEventListener('input', update);
update();
</script>
</body>
</html>
可以注意这几点:1fr 2fr 让第二列正好是第一列的两倍宽。200px 1fr 保留 200px,其余都给第二列。auto 1fr auto 让两侧的列按内容决定宽度。repeat(4, 100px) 会在末尾留下空白,因为没有任何一列是弹性的。无效的值(比如 1fr 2)会被忽略,网格保持上一个有效的值。
fr 单位
fr 的意思是“剩余空间中的一份”。浏览器先扣除固定宽度的列(px、rem、%、auto 内容)和间距,再按数字把剩下的部分分给各个 fr 列。
600px 容器中的 grid-template-columns,无间距 | 列宽 |
|---|
1fr 1fr 1fr | 200, 200, 200 |
1fr 2fr | 200, 400 |
100px 1fr 1fr | 100, 250, 250 |
1fr 50% | 300, 300 |
fr 和 % 不一样:百分比加上间距可能超过 100% 而溢出,而 fr 列始终只分配扣除间距之后剩下的空间。
repeat()、minmax() 与不用媒体查询的响应式网格
repeat(4, 1fr) 就是 1fr 1fr 1fr 1fr。minmax(120px, 1fr) 是一个永远不窄于 120px、并且会增长去占一份空间的列。把它们和 auto-fill 或 auto-fit(代替具体数量)组合起来,网格就会创建尽可能多的能放下的列:
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 16px;
}
这一条声明就能得到容器能容纳的、每列至少 180px 的若干列:宽屏上好几列,手机上一列,完全不需要媒体查询。当元素比列少时,auto-fill 和 auto-fit 的区别就显现出来了:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>auto-fill vs auto-fit</title>
<style>
body { font-family: system-ui, sans-serif; font-size: 13px; }
.g { display: grid; width: 560px; background: #e5e7eb; margin: 4px 0 14px; }
.fill { grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); }
.fit { grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); }
.g div { background: #2563eb; color: white; padding: 10px; outline: 1px solid white; }
</style>
</head>
<body>
<code>auto-fill</code>
<div class="g fill"><div>A</div><div>B</div><div>C</div></div>
<code>auto-fit</code>
<div class="g fit"><div>A</div><div>B</div><div>C</div></div>
<p id="out"></p>
<script>
document.getElementById('out').textContent = [...document.querySelectorAll('.g')].map((g) =>
g.className.split(' ')[1] + ': ' + [...g.children].map((c) => Math.round(c.getBoundingClientRect().width) + 'px').join(', ')
).join(' | ');
</script>
</body>
</html>
容器能放下五个 100px 的列。auto-fill 保留全部五列,所以 A、B、C 各 112px,剩下两个空列。auto-fit 折叠了空列,所以三个元素各拉伸到大约 187px。
放置元素:grid-column 与 grid-row
网格线从起始边开始编号,从 1 开始。grid-column: 1 / 3 表示“从第 1 条线到第 3 条线”,也就是两列。span 2 表示“从所在位置起跨两列”,-1 是最后一条线:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Spanning</title>
<style>
body { font-family: system-ui, sans-serif; font-size: 13px; }
.grid { display: grid; grid-template-columns: repeat(4, 1fr); grid-auto-rows: 60px; gap: 8px; }
.grid div { background: #16a34a; color: white; padding: 8px; }
.grid .wide { grid-column: 1 / -1; background: #111827; }
.grid .big { grid-column: span 2; grid-row: span 2; background: #2563eb; }
.grid .tall { grid-row: span 2; background: #f59e0b; }
</style>
</head>
<body>
<div class="grid">
<div class="wide">grid-column: 1 / -1</div>
<div class="big">span 2 x 2</div>
<div class="tall">grid-row: span 2</div>
<div>a</div>
<div>b</div>
</div>
</body>
</html>
深色元素占满整个第一行。蓝色元素覆盖一个 2 × 2 的区块,琥珀色元素跨两行。元素 a 和 b 会自己填入剩下的单元格:grid 会把每个能放的元素放进下一个空闲单元格。grid-auto-rows: 60px 设置那些没有用 grid-template-rows 声明的行的高度。
grid-template-areas:给布局的各部分命名
做页面布局时,给各区域命名,再用文字把它们“画”出来。每个字符串是一行,每个单词是一个单元格,元素用 grid-area 加入某个区域:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Grid areas</title>
<style>
body { font-family: system-ui, sans-serif; margin: 0; }
.page {
display: grid;
grid-template-columns: 160px 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header"
"side main"
"footer footer";
gap: 8px;
min-height: 280px;
}
header { grid-area: header; background: #111827; color: white; }
aside { grid-area: side; background: #e5e7eb; }
main { grid-area: main; background: #bfdbfe; }
footer { grid-area: footer; background: #111827; color: white; }
.page > * { padding: 12px; }
</style>
</head>
<body>
<div class="page">
<header>Header</header>
<aside>Sidebar</aside>
<main>Main content</main>
<footer>Footer</footer>
</div>
</body>
</html>
页头和页脚横跨两列,因为它们的名称填满了所在行的两个单元格。中间一行是 1fr,所以它占据 280px 中剩下的高度。要在手机上把所有内容堆叠起来,媒体查询只需把区域重新画成一列:grid-template-areas: "header" "side" "main" "footer" 加上 grid-template-columns: 1fr。媒体查询一页展示了这种写法。
在单元格内对齐
网格元素默认会拉伸填满所在的单元格。下面这些属性用来在单元格内移动它们:
| 属性 | 用在 | 移动 |
|---|
justify-items | 容器 | 每个元素沿行方向移动(行内轴) |
align-items | 容器 | 每个元素上下移动(块轴) |
place-items | 容器 | 简写:place-items: center 在两个方向上都居中 |
justify-self / align-self | 元素 | 单个元素 |
justify-content / align-content | 容器 | 当网格比容器小时,移动整组轨道 |
display: grid; place-items: center; 是在盒子中居中一个子元素最短的写法。
grid 还是 flexbox
grid 是二维的:你决定列(和行),内容去填充它们,所以画廊里的每张卡片宽度都相同,最后一行也是如此。flexbox 是一维的:元素沿一条线根据内容决定自身大小。两者可以很好地搭配:页面和卡片画廊用 grid,每张卡片内部的标题行用 flexbox。另一半内容见 flexbox 一页。
顺序与无障碍
grid-template-areas、grid-row、grid-column 和 order 可以把元素放在任何位置,但屏幕阅读器和 Tab 键仍然按照 HTML 的顺序来。让源码顺序保持有意义(页头、导航、主体、页脚),用 grid 来排布它,而不是用来弥补一个毫无道理的 HTML 顺序。
常见错误
- 把
display: grid 写在子元素上。 它应该写在父元素上;只有直接子元素才是网格元素。
- 百分比和间距一起用。
grid-template-columns: 50% 50% 加上 gap: 16px 会溢出 16px。请用 1fr 1fr。
- 想要
auto-fit 却用了 auto-fill。 元素少时,auto-fill 会留下空列。换成 auto-fit 让元素拉伸。
1fr 列超出了它应得的份额。 1fr 列不会缩到比它最长的内容(一个长 URL、一张宽图片)还窄。用 minmax(0, 1fr) 让它可以收缩。
- 区域字符串不是矩形。 每个命名区域都必须构成一个矩形,每行字符串的单元格数量也必须相同,否则整条
grid-template-areas 声明都会被忽略。
- 忘了网格线从 1 开始编号。 三列有四条线:
1 / 4 或 1 / -1 跨越全部三列。