Understand the coordinate system: tracks, gutters, and content boxes
The first principle of Grid is that the container is divided into row tracks and column tracks, and children are placed into grid areas formed by the intersections of row and column lines. All sizing is computed on tracks, not on the children themselves. The counter-intuitive part is that 1fr describes how wide the track is, not how wide the child is; the child ends up as the track width minus its own padding, border, and margin.
Tracks can be separated by a gutter, defined with gap. The gutter is not part of any track — it is empty space between them and is never occupied by content. That is why gap is preferable to per-item margins: margin-based spacing also appears at the start and end of the container unless you cancel it with negative margins, while gap has no such artifact and produces nothing at all when only one child is present.
Track sizes have exactly two sources: explicit definitions through grid-template-columns and grid-template-rows, and implicit definitions created by grid-auto-rows and grid-auto-columns for anything beyond the explicit tracks. Explicit tracks come first and implicit ones are appended after, and that explicit-then-implicit ordering determines where the auto-placement algorithm puts surplus items.
Two more concepts must be kept distinct: grid lines and grid areas. Column lines are numbered starting at 1, and n lines define n-1 column tracks. That is why grid-column: 1 / 3 means "from the first line to the third line", spanning tracks 1 and 2 — the numbers refer to lines, not to cells. Getting this off-by-one wrong is the single most common Grid mistake and is worth double-checking every time.
.layout {
display: grid;
/* 显式列轨道 + 轨道间隙 */
grid-template-columns: 240px 1fr;
gap: 24px;
/* 隐式行轨道:未定义的行按这个尺寸生成 */
grid-auto-rows: minmax(120px, auto);
}
/* 网格线编号从 1 开始,横跨两个轨道要写 1 / 3 */
.sidebar {
grid-column: 1;
grid-row: 1 / 4; /* 跨第 1、2、3 行 */
}
.content {
grid-column: 2;
}
/* 也可以用具名区域,先声明再用 */
.layout {
grid-template-areas:
"header header"
"sidebar content"
"footer footer";
grid-template-columns: 240px 1fr;
}
.header { grid-area: header; }
.sidebar { grid-area: sidebar; }
.content { grid-area: content; }How fr is actually distributed, and why layouts overflow
fr expresses a proportion of the remaining space, not the width of an element. The resolution order is: every non-fr track (fixed lengths, percentages, auto, the fixed part of minmax) claims its space first, and whatever is left of the container is then divided in proportion to the fr values. When all tracks are fr, the base is the container width divided by the sum of the coefficients.
The direct consequence is that 1fr does not mean "half". In grid-template-columns: 1fr 1fr the columns are indeed equal, but in 1fr 200px the 1fr track receives whatever remains after 200px and the gap are taken. At the same time, the automatic minimum size — min-width: auto — gives an fr track a content-derived minimum. When intrinsic content is wider than the proportionally computed track, the track simply refuses to shrink. This is the real cause of most unexplained Grid horizontal overflow.
There are two fixes. Either give the track an explicit floor with minmax(0, 1fr), telling the browser that minimum sizing must not be driven by content; or set min-width: 0 (or overflow: hidden) on the child to override the min-width: auto default. In practice, writing minmax(0, 1fr) instead of a bare 1fr is a cheap habit that eliminates an entire class of overflow bugs.
The second trap is mixing fr with auto. In grid-template-columns: auto 1fr, the auto track sizes to the largest intrinsic width of its contents and 1fr takes the rest. That looks natural, but when the content width is unbounded — a long URL, an unbreakable token — the auto column consumes everything and the 1fr column is squeezed to nearly zero. The predictable approach is to bound the auto track too, as in minmax(0, auto) 1fr, or to compose an explicit bound from fit-content() and clamp().
.app {
display: grid;
/* auto 列给出上限,避免长内容把 1fr 挤没 */
grid-template-columns: minmax(0, auto) minmax(0, 1fr);
gap: 16px;
}
/* 保险写法:所有 1fr 都写成 minmax(0, 1fr) */
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 20px;
}
/* 经典溢出场景:Grid 子项默认 min-width: auto,长单词会撑破容器 */
.grid-item {
min-width: 0; /* 关键:解除内容驱动的最小宽度 */
overflow-wrap: anywhere;
}
/* 传统 Flex 里对应的坑与解法(对比记忆) */
.flex-item {
min-width: 0; /* Flex 子项同样需要手动归零 */
}
.flex-item img {
max-width: 100%;
height: auto;
}minmax, auto-fill, and auto-fit: the responsive trio
minmax(min, max) defines a track as a range between a floor and a ceiling. In minmax(0, 1fr) the zero floor overrides the automatic minimum size while the 1fr ceiling participates in the remainder distribution. minmax(200px, 1fr) expresses both "never narrower than 200px" and "absorb as much of the rest as possible". Use minmax(200px, 400px) when you need a hard cap, and minmax(min-content, 1fr) when the track should grow with its content before it shrinks.
repeat(auto-fill, <pattern>) counts how many copies of the pattern fit the container width and then fills the row with that many explicit tracks, even if some end up empty. auto-fit differs by collapsing those empty tracks and letting the populated ones expand to fill the container. Visually, when the number of items is below the row capacity, auto-fill leaves a gap while auto-fit stretches the items.
One counter-intuitive but important detail: the difference is most visible when the item count is less than or equal to the container capacity, and the two behave almost identically once items must wrap. A second trap is that when the container has zero width, or when items are display: none, auto-fit can compute zero tracks and stack everything on top of everything else. In collapsible sidebars and modals, always give the container a non-zero minimum width or switch to explicit column counts via media queries.
The most dependable combination in practice is to let media queries own breakpoints and let Grid own column arithmetic: a single column on narrow screens, and something like repeat(auto-fill, minmax(280px, 1fr)) on desktop. But when the design specifies exact column counts per breakpoint, write them out explicitly instead. Design-derived column widths are rarely equal divisions, so auto-fill equal division will simply not match the mockup.
/* ① 流式卡片网格:自动换列,无需媒体查询 */
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 20px;
}
/* ② 同样写法但元素少时铺满:auto-fit 会折叠空轨道 */
.toolbar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
}
/* ③ 显式断点:设计稿规定列数时更可靠 */
.page-grid {
display: grid;
grid-template-columns: 1fr;
gap: 16px;
}
@media (min-width: 768px) { .page-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1200px) { .page-grid { grid-template-columns: repeat(3, 1fr); } }
/* ④ 侧边栏 + 主内容区,minmax(0, 1fr) 防止内容撑破 */
.shell {
display: grid;
grid-template-columns: 0 minmax(0, 1fr);
transition: grid-template-columns 200ms ease;
}
.shell.is-open { grid-template-columns: 240px minmax(0, 1fr); }
@media (prefers-reduced-motion: reduce) {
.shell { transition: none; }
}Placement: grid-auto-flow and explicit item positioning
The default auto-placement strategy is grid-auto-flow: row — fill each row left to right, then wrap. Setting column fills top to bottom before moving to the next column. Either works for a feed, but the rhythm differs: row flow suits horizontally scanned lists, column flow suits vertically scrolling boards and column-oriented categories.
grid-auto-flow: dense makes later items backfill holes left by taller earlier ones. For a masonry gallery of uneven cards it noticeably reduces ragged gaps at the end of rows, at the cost of breaking the correspondence between visual order and DOM order — and Tab order follows suit. Use dense only for decorative tiles where sequence is irrelevant, and never on anything interactive.
Explicit placement uses grid-row-start, grid-row-end, grid-column-start, and grid-column-end, with the grid-row and grid-column shorthands. Values can be line numbers or span N. Negative numbers count backwards from the end of the explicit grid, which is the convenient way to build sticky headers and footers because you never need to know the total row count in advance.
Finally, grid-template-areas draws the layout with strings. It looks closest to the mockup and is the most readable to debug, at the price of strict column-count alignment per row (a single missing space is a parse error) and less flexibility when columns are generated in a loop. The practical rule: fixed layouts use areas, dynamic or variable-column regions use line numbers with span.
/* ① 看板:列优先 + 横向滚动 */
.board {
display: grid;
grid-auto-flow: column;
grid-auto-columns: 280px;
grid-template-rows: repeat(3, auto);
gap: 16px;
overflow-x: auto;
}
/* ② 瀑布流:行优先 + dense 回填空洞(仅装饰性图块) */
.masonry {
display: grid;
grid-auto-flow: row dense;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px;
}
/* ③ 负数网格线:无需知道总行数即可固定首尾 */
.report {
display: grid;
grid-template-rows: auto 1fr auto;
}
.report > .title { grid-row: 1; }
.report > .body { grid-row: 2; }
.report > .footer { grid-row: -1; } /* 从显式网格末端倒数第一个 */
/* ④ 具名区域:一眼看清布局 */
.page {
display: grid;
grid-template-areas:
"nav nav"
"side main"
"side foot";
grid-template-columns: 200px minmax(0, 1fr);
grid-template-rows: 56px 1fr auto;
min-height: 100vh;
}
.nav { grid-area: nav; }
.side { grid-area: side; }
.main { grid-area: main; }
.foot { grid-area: foot; }Centering recipes and when to choose Grid over Flexbox
The cleanest centering in the Grid era is to occupy one cell and center within it: display: grid with place-items: center centers horizontally and vertically in one declaration. It replaces the long-standing position: absolute plus top/left 50% plus translate(-50%, -50%) sequence, but without leaving the document flow, without requiring position: relative on the parent, and while still filling the available space.
When you need equal space on all sides, use place-content: center, which centers the entire track group; place-items: center centers each item within its own cell. Conflating these is the most common semantic mistake in Grid: to center content while letting the background fill the container, use place-items; to shrink the whole grid into a centered cluster, use place-content.
The division of labor between Grid and Flexbox has a simple test: Grid is two-dimensional and describes row and column structure, while Flexbox is one-dimensional and handles alignment and distribution along a single axis. In practice, if you are writing justify-content, the item in question is almost certainly in a Flexbox row. If you are writing grid-template-columns, it is almost certainly a Grid layout. Mixing the two for the same row is where most layout bugs come from.
The selection rule reduces to one sentence: use Grid when you are describing "how many rows and columns, and how big each cell is"; use Flexbox when you are describing "how this run of items is distributed along the main axis". The most common real combination is Grid on the outside to carve the page into regions — sidebar, main, footer — with Flexbox inside each region for button groups, tag rows, and forms.
/* ① 通用居中:Grid 版一行搞定,不脱离文档流 */
.center-box {
display: grid;
place-items: center; /* 网格项在其格子内水平垂直居中 */
min-height: 320px;
}
/* ② 整个网格聚成一小团居中:用 place-content,不是 place-items */
.login-page {
display: grid;
place-content: center; /* 轨道组整体居中 */
grid-template-columns: 320px;
min-height: 100vh;
}
/* ③ 经典等式:外层 Grid 分区块,内层 Flex 排单方向 */
.toolbar {
display: flex;
align-items: center;
gap: 8px;
}
.toolbar__spacer { margin-left: auto; } /* 把右侧操作推到最右 */
/* ④ 移动优先的两列自适应,卡片最小宽 260px */
.split {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 20px;
}
/* ⑤ 尊重减少动效偏好 */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}