ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CSS九宫格:Grid/Flex/Table/Float等五种方案

CSS九宫格:Grid/Flex/Table/Float等五种方案 九宫格这个需求做前端的人基本躲不掉。商品列表、功能入口、抽奖转盘、相册预览、数据看板随便拎一个出来都是三行三列甚至更多行更多列。很多人第一次写 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-wrapflex-basis一行搞定改计算式IE10 起IE11 有若干已知问题移动端列表、卡片流table 布局table-layout: fixed天然支持改单元格宽度几乎全覆盖老系统、需要严格对齐的报表float 浮动定位floatwidth做不到需额外处理改宽度和 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 classgrid div classitem1/div div classitem2/div div classitem3/div div classitem4/div div classitem5/div div classitem6/div div classitem7/div div classitem8/div div classitem9/div /divrepeat(3, 1fr)的意思是重复三次每次占一份剩余空间。1fr里的fr是分数单位它的分配逻辑是容器宽度减去所有固定的列宽和所有gap之后剩下的空间按份数分。所以三列永远等宽而且总宽度永远不会超出容器——gap是从剩余空间里扣的不占百分比额度。这里有个很多人没注意的细节gap和margin在计算上的地位完全不同。假设容器宽 360pxgap: 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 */ }用具体数字验证一遍。容器宽 360pxgap10px可用宽度是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 classtbl tr td1/td td2/td td3/td /tr tr td4/td td5/td td6/td /tr tr td7/td td8/td td9/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: collapseborder-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 styleclear: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的倍数全部跟着走维护成本直接降一大截。我在几个长期迭代的项目里都用这个方式每次改版省下来的时间加起来相当可观。
返回列表