☰
CSS九宫格:Grid/Flex/Table/Float等五种方案
2026/9/30 12:36:59 网站建设 项目流程

九宫格这个需求,做前端的人基本躲不掉。商品列表、功能入口、抽奖转盘、相册预览、数据看板,随便拎一个出来都是三行三列,甚至更多行更多列。很多人第一次写 CSS 九宫格布局,都是拿百分比宽度加 float 一通怼,本地看着挺齐,上线换个字号或者多一个字就错位了。这篇文章就把我这些年攒下来的五种做法完整摊开讲:grid 布局、flex 布局、table 布局、float 浮动定位、inline-block 配合 letter-spacing,每种都给出可直接复制的代码、宽度计算过程、真实踩过的坑,以及什么场景该用哪一种。不管你是刚学 CSS 布局的新手,还是接手老项目要做兼容的熟手,看完都能直接抄作业。

1. 九宫格布局到底难在哪:从需求拆解到方案选型

九宫格看起来是个很简单的需求,无非就是九个方块排成三行三列。但真动手写就会发现,麻烦的地方从来不是"排成三行三列",而是那些附加要求:格子之间的间距要一致、每格宽度要相等、容器宽度变了要跟着变、内容多少不一还得对得齐。这四件事叠加起来,才让九宫格成了 CSS 布局的经典考题。

1.1 一个九宫格,拆开看只有三个变量

我把九宫格拆解成三个独立的变量来看,思路会清楚很多。

第一个变量是列数与行数。九宫格默认 3×3,但实际项目里经常变成 3 行 4 列、4 行 2 列,甚至用媒体查询在手机上变 2 列、平板上变 3 列、桌面上变 4 列。这意味着方案本身要能低成本地改列数,而不是改一次就要重算一遍宽度。

第二个变量是间距的处理方式。间距是九宫格最容易翻车的地方。用margin加百分比宽度的组合,稍不留神就是33.33% × 3 + 20px > 100%,第三列直接被挤到第二行去。而用gap或者表格的border-spacing,间距是从剩余空间里扣的,不会参与宽度分配,天然不会溢出。

第三个变量是单元格内部的对齐。格子里可能是一张图片加一行文字,也可能是纯文字可能只有两个字。文字长一点、短一点,方块高度就会不齐;默认的基线对齐还会让第二行的位置怪怪地往下掉。垂直居中这件事,不同方案的实现成本差异极大——表格一个vertical-align: middle就完事,float 则几乎做不到。

提示:先把这三个变量列清楚,再去看五种方案,你会发现每种方案的差异,本质上就是它解决这三个变量的成本不一样。

1.2 五种方案的取舍逻辑

为什么会有五种做法?因为它们诞生的年代不同,解决的问题侧重也不同。我按兼容性和实现难度拉了个表,先有个整体印象。

方案核心属性垂直居中改列数成本浏览器兼容下限典型场景
grid 布局grid-template-columns一行搞定改一个数字现代浏览器(IE11 部分支持,需前缀)新项目、后台看板、图库
flex 布局flex-wrap+flex-basis一行搞定改计算式IE10 起(IE11 有若干已知问题)移动端列表、卡片流
table 布局table-layout: fixed天然支持改单元格宽度几乎全覆盖老系统、需要严格对齐的报表
float 浮动定位float+width做不到,需额外处理改宽度和 nth-child全覆盖维护十年以上的老代码
inline-block + letter-spacingdisplay: inline-block+ 负字距需vertical-align改宽度全覆盖需要支持极旧环境

选型的逻辑其实很直接:新项目一律先考虑 grid,其次是 flex,另外三种基本只在维护历史代码时才会碰到。反过来说,如果你接手的是一个还在跑 IE8 兼容模式的后台系统,那 float 和 table 就是你必须掌握的生存技能,而不是"过时的东西没必要学"。

2. Grid 布局:把九宫格写成一行的方案

Grid 是专门为二维布局设计的,九宫格正好是它的主场。用 grid 写九宫格,核心代码只有三行,而且几乎不需要关心宽度计算——这是它跟 float 最大的区别。

2.1 最小可用代码与 gap 的真实作用

先看最小可用的版本:

.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }

配上一个简单的 HTML 结构:

<div class="grid"> <div class="item">1</div> <div class="item">2</div> <div class="item">3</div> <div class="item">4</div> <div class="item">5</div> <div class="item">6</div> <div class="item">7</div> <div class="item">8</div> <div class="item">9</div> </div>

repeat(3, 1fr)的意思是"重复三次,每次占一份剩余空间"。1fr里的fr是分数单位,它的分配逻辑是:容器宽度减去所有固定的列宽和所有gap之后,剩下的空间按份数分。所以三列永远等宽,而且总宽度永远不会超出容器——gap是从剩余空间里扣的,不占百分比额度。

这里有个很多人没注意的细节:gap和margin在计算上的地位完全不同。假设容器宽 360px,gap: 10px,一行三个格子只有两个间隙,也就是 20px 被扣掉,每格拿到(360 - 20) / 3 = 113.33px。而如果你写成width: 33.33%; margin-right: 10px,总占位就是33.33% × 3 + 30px,直接超过 100%,换行是必然结果。这也是为什么我一再强调,能用gap就别用margin。

注意:gap在 flex 布局里的支持比 grid 晚,Safari 14.1 之前不认 flex 的gap。grid 的gap支持要早得多,所以做 grid 布局时用gap基本不用担心。

2.2 间距、留白与响应式的几种常见变体

实际项目里九宫格不会只有一种形态,下面这几种改法都用得很多。

行列间距分开控制,写row-gap和column-gap就行:

.grid { display: grid; grid-template-columns: repeat(3, 1fr); row-gap: 16px; column-gap: 8px; }

格数跟着容器宽度自动变化,用auto-fit加minmax():

.grid-auto { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 10px; }

这行的意思可以翻译成大白话:每列最少 110px,最多占一份剩余空间,能塞几列就塞几列,塞不下就换行。容器 360px 时,(360 - 2×10) / 110 ≈ 3.09,所以是三列;容器缩小到 240px 时变成两列。这比写一堆媒体查询要省事得多。

让某个格子跨列或跨行,用grid-column和grid-row:

.item-large { grid-column: span 2; grid-row: span 2; }

配上grid-auto-rows: 1fr或者固定行高,就能做出那种"第一个格子特别大"的不规则九宫格。这也是 grid 相比另外四种方案最不可替代的地方——float 和 inline-block 想做跨格,得靠 JS 算位置或者写死宽高。

实操心得:我在做后台数据看板时,习惯给容器加一句align-items: stretch(这本来就是默认值,写出来是为了提醒自己),再给格子加min-height,这样即使某一行只有文字很短的格子,整行高度也保持一致,视觉效果会整齐很多。

3. Flex 布局:兼容与灵活之间的平衡点

flex 是做一维排列的,本身不擅长二维网格,但配合flex-wrap: wrap之后,做九宫格也没问题。它的优势是兼容性比 grid 好,语法比 float 干净;劣势是宽度必须自己算,最后一行不满时还得额外处理。

3.1 三列等宽的宽度计算过程

先看容器:

.flex-wrap { display: flex; flex-wrap: wrap; gap: 10px; }

关键在子元素的宽度。一行三格只有两个间隙,所以正确的算法是:

.flex-wrap .item { flex: 0 0 calc((100% - 20px) / 3); /* 也就是 (100% - 2 个 gap) / 3 */ }

用具体数字验证一遍。容器宽 360px,gap10px:可用宽度是360 - 20 = 340px,每格340 / 3 = 113.33px,三格加两个间隙正好113.33 × 3 + 20 = 360px,严丝合缝。

如果换成四列呢?一行四个间隙变成三个,公式跟着变成calc((100% - 30px) / 4)。规律很简单:减掉的是(列数 - 1)个 gap。很多人在这里出错,是因为脑子里默认"减掉列数个 gap",结果算出来每格窄了一点,右边空出一条缝,视觉上还能看,但严格来讲已经不对齐了。

提示:flex: 0 0 calc(...)里的三个值分别是flex-grow、flex-shrink、flex-basis。写成0 0是明确告诉浏览器"不许放大也不许缩小",宽度完全由flex-basis决定。如果用简写flex: 1,浏览器会把flex-basis设成0%,宽度分配逻辑就完全变了,九宫格会被撑得乱七八糟。

3.2 换行、间隙与最后一行对齐的补偿技巧

flex 做九宫格最常见的两个问题,一是最后一行不满时的对齐,二是老浏览器不支持gap。

最后一行不满:假设九个格子,容器一行只能放四个,那第三行只有一个格子,它会靠左,右边空出三格。多数设计稿期望的也是这样,但如果你希望它居中或者均分,就得用::after占位:

.flex-wrap::after { content: ""; flex: 0 0 calc((100% - 20px) / 3); }

占位元素的数量要按"一行几个减掉最后一行的个数"来算。这种技巧能用,但我个人不太喜欢,因为它把结构搞复杂了,而且列数一变就得重新算。真遇到这种需求,我会直接换成 grid。

老浏览器没有gap:兜底方案是回到margin,但这时宽度公式必须跟着改。比如用margin-right: 10px,那三格的宽度就得是calc((100% - 20px) / 3),同时把第三列的margin-right去掉:

.no-gap .item { width: calc((100% - 20px) / 3); margin-right: 10px; margin-bottom: 10px; } .no-gap .item:nth-child(3n) { margin-right: 0; }

注意nth-child(3n)这类选择器一旦列数从 3 变成 4,就得跟着改成4n,维护成本明显比gap高。这也是我建议新项目直接用 grid 的原因之一。

4. Table 布局:最老但对齐最稳的一条路

Table 布局在很多人印象里是"上个时代的东西",但在需要严格对齐的场景里,它依然好用到让人舍不得换。行高天然一致、垂直居中一行搞定、内容撑不破宽度,这三点加起来,在很多老后台系统里比 flex 还省心。

4.1 真 table 标签的写法与 table-layout: fixed 的意义

最直接的写法就是用真的表格标签:

<table class="tbl"> <tr> <td>1</td> <td>2</td> <td>3</td> </tr> <tr> <td>4</td> <td>5</td> <td>6</td> </tr> <tr> <td>7</td> <td>8</td> <td>9</td> </tr> </table>

样式上有个属性必须加:

.tbl { width: 100%; table-layout: fixed; border-collapse: separate; border-spacing: 10px; } .tbl td { width: 33.33%; vertical-align: middle; text-align: center; }

table-layout: fixed是这套方案能不能用的分水岭。不加它,浏览器会按"内容优先"的算法分配列宽——哪个格子里的文字长,哪一列就更宽,九宫格立刻变成"三列不等宽"。加上它之后,列宽完全由你指定的33.33%决定,内容再长也只会在格子内部换行或者溢出,不会影响其他列。

border-spacing是表格版的gap,它只在border-collapse: separate时生效。如果你习惯性写上border-collapse: collapse,border-spacing会直接失效,表现就是九个格子紧紧贴在一起,间距全没了。这是个非常经典的坑,我自己就在这儿浪费过半小时。

4.2 display: table 系列的 div 模拟与垂直居中红利

不想用真表格标签的话,可以拿 div 模拟:

.tbl-sim { display: table; width: 100%; table-layout: fixed; border-spacing: 10px; } .tbl-sim .row { display: table-row; } .tbl-sim .cell { display: table-cell; width: 33.33%; vertical-align: middle; text-align: center; }

这种写法的最大红利是垂直居中。vertical-align: middle在table-cell上就是真正意义上的垂直居中,不需要line-height等于行高,也不需要transform: translateY(-50%),内容多高都能自动居中。对于格子内容高度不一致的九宫格(比如有的格子一行字,有的三行字),这一点太省事了。

需要提醒的是,display: table的盒子在宽度计算上会有自己的脾气:它默认是"收缩到内容宽度"的,所以width: 100%基本是必须写的。另外,用 CSS 生成的匿名表格盒子,在某些极端嵌套情况下会产生意料之外的换行,调试时用浏览器的元素检查器看一眼实际生成的盒子结构,会省很多功夫。

注意:table 系列方案的min-width行为跟普通块级盒子不一样,它会受内容影响。如果某个格子放了特别长的英文串或者不换行的数字,列会被撑宽。加word-break: break-all或者overflow-wrap: anywhere到td上是常规做法。

5. Float 浮动定位:老项目里用得最多的续命方案

float 是最早被拿来排版的属性,本质上它是为"文字环绕图片"设计的,被硬生生用成了布局工具。用 float 做九宫格能跑,但必须自己处理宽度、间距和清除浮动这三件事,一步没做到位就会出问题。

5.1 宽度与外边距的配对计算

基础代码大概是这样:

.float-box { overflow: hidden; /* 先给它一个 BFC 兜着 */ } .float-box .item { float: left; width: calc((100% - 20px) / 3); margin-right: 10px; margin-bottom: 10px; box-sizing: border-box; } .float-box .item:nth-child(3n) { margin-right: 0; }

宽度计算逻辑和 flex 版完全一致:减掉两个间隙再除以三。但 float 有一个额外的要求——每行最后一个必须把margin-right清掉,不然它的右外边距会残留,虽然视觉上看不出来(因为右边本来就没东西),但在某些情况下会影响换行判断,尤其在间距改大之后。

更稳的写法是给容器用负外边距法,也就是"容器margin-right: -10px,子元素统一margin-right: 10px",这样不用写nth-child,列数改了也不用动选择器。代价是容器的实际宽度超出了父级,如果父级有overflow: hidden,右边会被裁掉 10px,需要小心处理。

我个人的经验是:float 方案里,box-sizing: border-box一定要全局打开。因为九宫格里的卡片十有八九要加 padding 和内边框,如果不设成 border-box,实际占位宽就变成了113.33 + 20 + 2 = 135.33px,三格直接超宽换行,而且这种问题在只加 1px 边框的时候特别隐蔽。

5.2 清除浮动、高度塌陷与 BFC 的副作用

float 的子元素会脱离文档流,容器不会把它们的高度算进去,这就是"高度塌陷"。表现是容器背景色、边框、下方内容全部压上来,看起来像没写布局一样。

清理方式有三种,按年代排:

方式写法问题
额外标签末尾加<div style="clear:both"></div>污染结构
overflow父元素overflow: hidden会裁剪阴影、下拉菜单、绝对定位子元素
伪元素::after { content:""; display:block; clear:both; }最推荐

伪元素法是我现在唯一会用的方式:

.clearfix::after { content: ""; display: block; clear: both; }

overflow: hidden虽然代码短,但它的副作用在九宫格场景里特别容易爆发:卡片通常都有box-shadow和鼠标移入放大效果,overflow: hidden会把这些超出容器的部分统统剪掉,视觉上卡片像被"削掉一块"。另外,如果格子里有绝对定位的角标、标签,跑到容器外面也会被裁剪。这些都是我在老项目里翻过车的具体情形。

6. inline-block + letter-spacing:被误传成黑科技的经典技巧

这套组合经常被写成"CSS 小技巧"到处流传,看代码的时候会觉得很神奇,理解了原理之后其实一点也不神秘。它解决的只是行内块元素之间的空白间隙问题。

6.1 那个多出来的空隙到底是什么

先用最基本的写法看看现象:

.ib { font-size: 0; /* 一种常见的处理方式 */ } .ib .item { display: inline-block; width: 33.33%; vertical-align: top; }

之所以要处理"空隙",是因为 HTML 源码里标签之间的换行符和空格在渲染时会被当成一个空白字符,行内块元素之间的这个空白字符会实打实地占宽度——大约是当前字体下空格字符的宽度,常见字体里差不多是 0.25em 左右。三个 33.33% 的格子之间夹两个空格,总宽就超过了 100%,第三个格子被挤到第二行。

处理办法有几种:把 HTML 里标签之间的换行全删掉(可读性差到没法维护)、父元素font-size: 0、或者用本文提到的letter-spacing方案。font-size: 0的问题是会被子元素继承,子元素里所有文字都得重新设字号,一旦漏设,文字直接消失,排查起来很懵。

6.2 负 letter-spacing 的原理、取值与三个容易翻车的点

letter-spacing控制的是字符间距。给父元素设一个负值,等于把每个字符之间的距离缩短,行内块之间的那个空白字符自然也跟着变窄。写成代码:

.ib-spacing { letter-spacing: -0.5em; font-size: 0; /* 保险起见 */ } .ib-spacing .item { display: inline-block; width: 33.33%; letter-spacing: normal; /* 关键:把文字的字距还原 */ vertical-align: top; box-sizing: border-box; }

原理拆开就是两步:父元素的负字距把空白字符"压缩"掉,子元素再把letter-spacing重置回normal,避免自己的文字被挤成一团。子元素那句letter-spacing: normal绝对不能漏,因为letter-spacing是可继承属性,漏了之后格子里的中文会挤在一起,标题看着像出了 bug。

三个容易翻车的点:

第一,负值大小依赖字体。空格宽度在不同字体、不同字号下并不一致,-0.5em是个经验值。取值偏小,空格没吃干净,第三格会掉到下一行;取值偏大,格子之间会产生负间隙,表现为内容被裁掉一点或者布局轻微左偏。稳妥的做法是取一个略小的值,比如-0.4em,宁可留一丝缝也别吃掉内容。

第二,压缩过的 HTML 会让这套写法失效。如果你的构建流程会把 HTML 标签之间的换行去掉,那空格本来就不存在,负字距反而制造了多余的负空间,格子会整体往左偏。这类问题本地开发看不出、打包上线才出现,特别难查。

第三,垂直对齐默认是基线对齐。行内块的默认vertical-align: baseline,会导致下一行的第一个格子根据上一行的文字位置"跳"一下,看起来行距忽大忽小。统一写vertical-align: top就能把这排齐。

7. 五种方案横向对比:同一份九宫格,五份代码的实测差别

光看代码不够直观,我把同一份需求用五种方案各实现了一遍,在同一个页面里并排跑,记录了一些实际差异。

7.1 对比表与实测数据

测试环境是 360px 宽的容器,gap统一 10px,九格内容一致(每个格子一行两字标题加一行说明)。实测结果如下:

方案单格宽度改四列需要动几处内容变长时的表现是否等高
grid113.33px1 处行高自动撑开,其余格子跟着变高是
flex113.33px1 处(计算式)同上是
table113.33px1 处(width)单格内换行,列宽不变是
float113.33px2 处(宽度 +nth-child)单格变高,其他格子高度不变,行高参差否
inline-block113.33px1 处同 float,行高参差否

"是否等高"这一栏是九宫格最容易被忽略的差别。grid、flex、table 三种方案里,一行内所有格子默认高度一致,视觉上就是严格的网格;float 和 inline-block 里,格子各自按内容撑高,一行里可能一个高 60px、一个高 80px,下一行就参差不齐了。要修就得用 JS 算最大高度,或者给所有格子写死height,这两种做法都很难维护。

7.2 不同项目场景下的选型建议

结合上面这些差异,我总结了几条实际的选型经验。

新写的页面、不需要兼容旧浏览器:直接用 grid。代码最短,改列数只动一个数字,跨格布局也有现成属性。这一条适用于绝大多数 2020 年之后的新项目。

需要较好的兼容性又要写得舒服:用 flex。IE11 虽然有几个已知的flex-basis计算问题,但通过把flex-basis写成具体的calc()而不是百分比,基本能绕过去。移动端列表、卡片流用 flex 非常合适。

格子内容高度不可控,且要求整行严格对齐:table 布局最省事。很多后台系统的报表、参数展示页到现在还在用表格,不是守旧,是因为它确实好用。

维护老项目:大概率你会遇到 float。这时别急着重构,先按现有写法把nth-child和clearfix补齐,把它跑稳,再考虑是否分批替换。

极旧的环境,连calc()都不支持:只能上 inline-block 配负letter-spacing,或者老老实实写死像素宽度。这种情况下,固定宽度反而是最可靠的选择。

8. 常见问题与排查实录

下面这些是我在实际项目里真实撞见过的问题,按出现频率排了个序,附上排查思路。

8.1 高频问题速查表

现象常见原因处理方式
第三格掉到第二行宽度加边距超过 100%改用calc()扣掉间隙,或改用gap
格子之间的间距不等用nth-child清边距时列数对不上确认选择器的倍数与列数一致
一行里格子高度参差float / inline-block 不等高换 grid、flex 或 table,或统一min-height
设置gap没效果flex 里老浏览器不支持,或父元素不是 flex/grid用margin兜底
表格间距消失border-collapse: collapse覆盖了border-spacing改成separate
格子内容把列撑宽表格未设table-layout: fixed加上它,并配word-break
卡片阴影被切掉父元素overflow: hidden改用clearfix伪元素清浮动
文字挤在一起负letter-spacing被继承子元素重置letter-spacing: normal
打包上线后布局错位HTML 压缩去掉了标签间空白不要依赖空白字符做布局

8.2 几个只有动手做过才知道的细节

第一,gap到底扣在哪一侧。用 grid 或 flex 的gap时,间距只出现在格子之间,容器的最左、最右、最上、最下都没有。这一点和"每个格子都有margin: 10px"的效果完全不同:后者会让容器左右各多出 10px 的空隙,导致内容整体比设计稿窄 20px。如果你按设计稿的容器宽度去核像素,永远差那么一点,问题往往就出在这里。

第二,格子内图片的处理。九宫格里放图片是最常见的情况,图片不处理的话会出现"图片把格子撑高"或者"图片比格子窄,右边留白"。我习惯给图片加两句:

.item img { display: block; width: 100%; height: 100%; object-fit: cover; }

display: block是为了去掉行内图片底部的基线空隙,那个空隙大约 3 到 5 像素,肉眼可见但很难想到原因。object-fit: cover保证图片按比例填满格子、裁掉多余部分,避免被拉变形。

第三,鼠标移入放大时的层叠问题。九宫格卡片加载入放大的效果很常见,但一旦放大,卡片会盖住旁边的内容,或者反过来被后面的卡片盖住。解决办法是给.item加上position: relative,在:hover时把z-index提高:

.item { position: relative; transition: transform 0.2s ease; } .item:hover { transform: scale(1.05); z-index: 2; }

顺序不能反——z-index只在定位元素上生效,所以position: relative必须写在基础样式里。

第四,用浏览器工具核对像素。布局对不齐的时候,别凭感觉调数值。打开开发者工具,选中容器,看它的实际内容宽度;再选中格子,看getBoundingClientRect给出的实际宽度和位置。你会发现很多时候差的不是宽度,而是某个看不见的margin或者padding在作怪。我排查九宫格问题基本都遵循同一个顺序:先看容器宽度,再看单个格子的实际占位宽,最后拿着"占位宽 × 列数 + 间隙总数"跟容器宽度对一遍,通常一眼就能看出问题在哪。

第五,列数一定要在代码里集中管理。不管你用哪种方案,只要列数写死在多个地方,后面改需求就一定会漏改。用 CSS 变量把列数抽出来是个成本很低的好习惯:

:root { --cols: 3; --gap: 10px; } .grid { display: grid; grid-template-columns: repeat(var(--cols), 1fr); gap: var(--gap); }

改列数的时候只动--cols一个值,宽度计算、nth-child的倍数全部跟着走,维护成本直接降一大截。我在几个长期迭代的项目里都用这个方式,每次改版省下来的时间加起来相当可观。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询