ARTICLE DETAIL

资讯详情

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

CSS九宫格布局五种方案对比与选型

CSS九宫格布局五种方案对比与选型 做前端这些年被问得最多的一类问题不是某个框架怎么用而是这个布局你一般怎么写。九宫格就是其中的高频选手——从移动端的金刚区导航、商品分类入口到PC端的图片墙、功能面板几乎每个项目里都会出现。真要动手的时候你会发现css九宫格布局至少有五种写法能跑通grid布局、flex布局、table布局、float浮动定位以及inline-block配合letter-spacing抵消间隙的老办法。五种都能出结果但选哪个、为什么选它、写完会不会在某个尺寸下崩掉才是真正拉开差距的地方。这篇就把这五种方案从原理到代码、从适用场景到踩坑细节全部摊开讲一遍不管你是刚学CSS没多久、还在纠结浮动怎么清除的新手还是写过几年项目、想回头把基础补齐的老手都能在里面找到自己需要的那一段。1. 先想清楚九宫格的本质约束到底是什么1.1 拆开看九宫格只有两个技术难点很多人觉得九宫格简单不就是三行三列吗。但真正动手写过的人知道麻烦从来不在排成三列这件事上而在两个隐藏的地方。第一个难点是等分与自适应的矛盾。容器宽度是未知的可能是375px的手机屏也可能是1920px的显示器你怎么让每个单元格自动算出自己的宽度百分比看似能解决但一旦涉及间距百分比的分母就变了33.33%加上10px的 margin一行下来必然超过100%。这就是为什么纯百分比方案总是会在某些宽度下换行。第二个难点是间距与边缘的关系。九宫格通常每行之间需要间隙但最左边和最右边又不能有额外的外边距。这个中间有间隙、两边不能有的需求是浮动方案、inline-block方案必须绕着走的坎也是grid和flex用gap一行搞定的地方。有些细节必须先明确否则后面五种方法根本没法横向比较。1.2 动手前必须定下来的三个前提第一间距算在宽度内还是宽度外。如果单元格宽度是33.33%再加上左右各8px的间距那实际占位就超过了三分之一必然挤掉最后一个。所以老方案的通用做法是宽度给足用负 margin 或负 letter-spacing 把间距从容器外侧拉回来后面讲float和inline-block时会详细拆。第二最后一行数量不足时怎么对齐。九宫格通常是9个正好铺满但现实里经常是7个、8个、10个。这时候最后一行是靠左、居中还是自动拉伸grid的默认行为是左对齐并保留空位flex的justify-content: space-between在最后一行只有一项时会让它贴在最左边其实是贴左因为它唯一而如果用space-around又会显得整体偏移。这个问题在选型阶段就要想清楚。第三单元格高度是否相等。图片墙场景下如果某个单元格内容多一点高度就可能被撑开导致整行参差不齐。grid的grid-auto-rows和flex的align-items: stretch行为完全不同这直接影响视觉整齐度。把这三个前提定死五种方案的优劣就变成了一道可以量化的选择题而不是凭感觉瞎试。1.3 五种方案的能力矩阵先给一张总览表后面每一节会展开讲实现细节和坑。方案间距处理响应式改列数等高能力兼容性推荐度gridgap 一行搞定改一个变量天然等高现代浏览器均支持首选flexgap 或负 margin改宽度百分比需要 stretch 配合极好次选tableborder-spacing改单元格宽度天然等高极好特定场景float负 margin 抵消改宽度百分比需额外处理极好不推荐新项目inline-block负 letter-spacing改宽度百分比需 vertical-align 配合极好了解即可这张表不是让你死记而是让你建立直觉grid是二维布局的原生解其余四种都是在一维思维上打补丁。理解这一点后面所有的为什么这样写就都能串起来了。2. Grid布局把九宫格当二维问题来解2.1 grid-template-columns才是九宫格的正确打开方式用grid做九宫格核心代码短到有点不真实.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }三行代码九个单元格自动排成三列。这里的关键是repeat(3, 1fr)里的1fr它的意思是剩余空间的一份。容器把可用宽度已经扣掉gap占用的部分平均分成三份每份就是一个单元格的宽度。重点是gap是被优先扣除的不像百分比那样需要自己去算分母。gap: 10px同时处理了行间距和列间距。它只在单元格之间产生间隙不会在外围多出边距这就直接解决了1.2提到的第二个前提。我见过不少人第一次用grid会觉得这么简单肯定有诈其实没有。真正需要理解的是fr与%的区别%是相对容器宽度的固定比例fr是在扣除固定尺寸和间距之后对剩余空间的分配。这个差异在混合使用固定宽度列的时候会非常明显比如grid-template-columns: 80px 1fr 1fr左侧固定80px右边两列分掉剩下的空间用百分比是做不到这种动态剩余的。2.2 aspect-ratio让正方形格子变成一行代码九宫格里最常见的视觉需求是每个格子都是正方形尤其是图标区。以前的做法是用 padding-top 撑高度的百分比技巧.cell { padding-top: 100%; position: relative; } .cell .inner { position: absolute; inset: 0; }这套写法至今在很多老项目里活着原理是padding的百分比是相对宽度计算的所以padding-top: 100%就等于高度等于宽度。但它需要多一层绝对定位的子元素来装内容写起来绕。现在直接用.cell { aspect-ratio: 1 / 1; }一行解决。aspect-ratio的取值是宽高比写1 / 1是正方形4 / 3是四比三。它和grid配合的时候如果单元格没有显式高度浏览器会根据宽度自动算出高度这也是grid方案里唯一需要注意的地方——aspect-ratio 需要单元格宽度是确定的而grid的1fr正好能提供一个确定的宽度值两者配合得非常顺。2.3 响应式改列数只动一个数字移动端想在窄屏显示三列、宽屏显示四列或五列grid的处理方式干净利落.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; } media (min-width: 768px) { .grid { grid-template-columns: repeat(4, 1fr); } }只需要改repeat()里的那个数字间距、高度、对齐全都不用动。对比一下float方案改列数要同时改宽度百分比、改nth-child的margin归零规则一处不对就整行走形。更进阶一点可以用auto-fill配合minmax()做不写媒体查询的响应式.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 12px; }意思是每列至少100px能塞几列塞几列剩下的空间平均分。这个写法在图片墙、卡片列表里非常实用容器一变宽列数自动增加完全不用维护断点。2.4 grid用起来最容易犯的几个错第一个坑是把 gap 写成了 margin。有些从flex迁移过来的同学习惯给子元素加margin-right结果在grid里就变成了gap加上margin的双重间距最右边还多出一块。grid下面就用gap别混着来。第二个坑是grid-template-areas 用错方向。grid-template-areas定义的是行优先的网格写九宫格的时候每行三个名字、写三行很容易因为缩进或换行搞乱。如果只是等分九宫格根本不需要grid-template-areas用repeat就够了别为了炫技把简单问题复杂化。第三个坑是高度不统一导致的错位。grid的默认align-items是stretch同一行的单元格会自动拉伸到最高那个的高度这通常是想要的效果。但如果你给某个单元格设了固定高度同一行就会出现高度不齐。这种情况下要么统一用min-height要么显式设置grid-auto-rows: 1fr让所有行等高。提示grid 的子项如果有绝对定位内容记得给单元格加position: relative否则绝对定位会跑到最近的定位祖先上去这是九宫格图标角标最常见的显示错误来源。3. Flex布局上手最快但最后一行容易翻车3.1 flex-wrap撑起一行三个的基本写法flex做九宫格的基本结构是这样.flex-grid { display: flex; flex-wrap: wrap; gap: 10px; } .flex-grid .cell { width: calc((100% - 20px) / 3); }flex-wrap: wrap允许换行gap: 10px提供间距单元格宽度用calc算。这里的20px是两行间隙的总和——一行三列有两个间隙所以从100%里减掉两个gap再除以三。calc里那个20px是硬编码的如果把gap改成12px就必须同步改这里这是flex方案最容易被忽略的维护点。解决办法是用CSS变量.flex-grid { --gap: 10px; gap: var(--gap); } .flex-grid .cell { width: calc((100% - var(--gap) * 2) / 3); }改间距的时候只动一个变量宽度自动跟着算。这个技巧在table和float方案里同样适用属于通用套路。3.2 最后一行数量不足时的对齐难题假设九宫格只有8个元素最后一行只有2个。用flex默认的justify-content: flex-start它们会靠左排列右边空一块视觉上没问题。但如果设计稿要求最后一行居中麻烦就来了。用justify-content: space-between会更糟前两行是正常的最后一行只有两项时第一项贴左、第二项贴右中间撑开一大块直接崩。想要最后一行居中常见的补丁方案是给最后一行占位.flex-grid::after { content: ; flex: 0 0 calc((100% - var(--gap) * 2) / 3); }或者用不可见的占位元素把行填满。但这类方案的问题是占位元素的数量取决于最多缺几个9个格子最多缺2个就得加两个占位写起来非常别扭。这就是flex一维本质的体现它按主轴方向排列不关心行这个概念所以无法感知这一行还剩几个位置。grid就不存在这个问题因为格子是按行列定义的最后一行不足时天然左对齐想居中也可以给容器加justify-content: center而不会影响其他行。3.3 单元格高度不一致的处理flex容器默认align-items: stretch同一行的项目会拉伸到最高那个。这跟grid的默认行为一样但flex有个隐藏问题如果单元格宽度用了calc而高度由内容决定一旦某行某个格子内容特别多那一行会被撑高而其他行的格子高度不变整体看起来就是这一行特别高。解决办法有两条路。一条是显式给单元格设min-height或固定高度简单直接但不够灵活。另一条是把内容用overflow: hidden或line-clamp限制保证所有格子高度一致。我一般会优先用后者因为九宫格里的文字通常是两三行就截断的控制行数比控制高度更符合内容逻辑。3.4 flex方案里几个真实踩过的细节flex-basis和width同时写的时候flex-basis优先级更高。如果你写了flex: 1又写了width: 33%实际宽度由flex: 1决定width只管初始值很容易出现我明明写了33%为什么不是三列的困惑。gap在老版本浏览器上不生效如果项目还需要支持很旧的移动端内核得退回负margin方案。flex容器里的元素如果设了margin: auto会和justify-content打架导致意料之外的居中效果。4. Table布局被嫌弃的老古董其实有专属位置4.1 display:table把表格语义搬到布局上table布局的核心不是用table标签而是在CSS里用display: table系列值模拟表格行为.table-grid { display: table; width: 100%; border-spacing: 10px; } .table-row { display: table-row; } .table-cell { display: table-cell; width: 33.33%; }border-spacing是表格模型特有的属性它在单元格之间产生间距效果和gap类似但要注意它同时在容器边缘也会产生间距——也就是说最左边和最右边也会多出10px。要消掉这个外围间距通常给容器加margin: -10px来抵消或者改用border-collapse: separate配合别的技巧。4.2 table为什么能天然等高table布局最独特的优势是同一行的单元格天然等高这是表格模型的固有行为不需要align-items或min-height任何额外设置。在做那种左边图片右边文字、高度必须一致的卡片时table方案反而比flex更省心。另外table-layout: fixed会让表格按照第一行的宽度分配来固定列宽不再根据内容自动调整。这在九宫格里很有用因为它能保证无论内容多少列宽都跑不掉。4.3 它在今天的合理使用场景我得诚实说整页布局用table是过时的做法原因很简单它无法在视觉顺序上和DOM顺序不一致时灵活调整对无障碍和响应式都不友好而且嵌套多层表格会让结构变得极难维护。但有一个场景它依然好用结构本身就是表格数据的展示比如价格对比表、参数表、排班表。这种情况下用真表格标签配合display: table-cell的间距控制语义和视觉能同时满足。九宫格如果恰好是3x3的数据矩阵用table也说得过去。还有一种情况是给邮件模板做布局。邮件客户端的CSS支持极其有限table是少数在几乎所有邮件客户端里都能稳定渲染的方案。这个场景下table不是老古董而是唯一解。5. Float浮动定位理解它才知道为什么不选它5.1 float nth-child归零margin的老套路浮动方案的历史最久写法长这样.float-grid { overflow: hidden; } .float-grid .cell { float: left; width: 33.33%; box-sizing: border-box; padding: 0 5px; } .float-grid .cell:nth-child(3n) { padding-right: 0; }思路是每个格子占三分之一宽用box-sizing: border-box把padding算进宽度里这样就不会撑破。然后通过nth-child(3n)把每行最后一个格子的右侧内边距去掉。这里有个容易搞混的点为什么用padding而不是margin因为margin会加到宽度外侧33.33% 10px必然超过三分之一导致换行。而box-sizing: border-box下的padding是算在宽度内的宽度不变视觉上就有了间隙。理解这一点你就理解了浮动方案里所有负margin技巧的由来。5.2 负外边距抵消法怎么算另一种更规整的浮动写法是给容器加负margin.float-wrap { margin-right: -10px; } .float-wrap .cell { float: left; width: calc(33.333% - 10px); margin-right: 10px; }每个格子右边留10px最后一列的右边也多留了10px但容器的margin-right: -10px把它拉回来了视觉上最右边对齐。这个正负抵消的思路在栅格系统里非常常见Bootstrap的早期版本就是这么干的。关键在于理解负margin不会改变元素本身宽度只是让容器边界往右扩了10px于是子元素多出来的那10px被藏到了容器外面。5.3 清除浮动这件事为什么必须做浮动元素会脱离普通流父容器如果不处理高度会塌陷成0后面的内容直接贴上来。清除方式主要有三种方式写法特点overflow父容器加overflow: hidden最简但会裁切溢出内容clearfix伪元素::after { content: ; display: block; clear: both; }通用无副作用加空div末尾加div styleclear:both/div有效但污染结构现代项目里推荐clearfix的伪元素写法overflow: hidden虽然简单但一旦某个子元素需要做超出容器的定位比如tooltip就会被切掉这是很容易在后期踩到的坑。5.4 浮动方案今天还有没有价值坦白讲新项目不该用浮动做九宫格。它的宽度需要手算改列数要改两处清除浮动还得额外处理对齐最后一行基本没法优雅实现。它的残留价值在于读懂老项目。你现在接手一套五年前的后台管理系统里面到处是float: left加clearfix如果不懂这套逻辑改一个间距可能要花半天。所以学浮动不是要用它而是为了维护存量代码时能快速定位问题。6. inline-block配letter-spacing最偏门的一招6.1 那个神秘的空白间隙到底从哪来先看一段代码div classinline-grid div classcell/div div classcell/div div classcell/div /div.inline-grid .cell { display: inline-block; width: 33.33%; }理论上三个33.33%应该正好一行但实际渲染会换行。原因在于HTML标签之间的换行和空格也是文本节点在inline-block布局下会被渲染成一个空格字符这个空格占了几像素宽度三个格子加上两个空格总量超过100%于是第三个被挤到下一行。这是CSS里最经典也最让人摸不着头脑的现象之一。理解了行内元素的空白符会占位这一点后面所有方案都是围绕怎么消掉它。6.2 负letter-spacing抵消间隙的原理最聪明的解法是给父容器设负的字间距.inline-grid { letter-spacing: -0.5em; } .inline-grid .cell { display: inline-block; width: 33.33%; letter-spacing: 0; vertical-align: top; }letter-spacing控制字间距设成负值会让每个字符之间的间距减少包括那些由换行产生的空白符。父容器上设负值间隙被吸掉子元素上必须重置为0否则格子内的文字也会挤在一起。这个负值具体取多少取决于字体大小。常见做法是-0.5em或者更狠的-1em因为不同字体的空格宽度不一样所以这个值需要实测调整没有标准答案。这也是它最大的缺点——跨字体不稳定。6.3 font-size:0方案的副作用另一种写法更简单粗暴.inline-grid { font-size: 0; } .inline-grid .cell { display: inline-block; width: 33.33%; font-size: 14px; }父容器字体设为0空白符自然就不占宽度了。但副作用很实在所有继承的字体大小都变成0子元素必须逐个重新声明font-size。如果格子里还有嵌套元素用了em单位会因为父级字号是0而全部变成0导致整块内容消失。这类问题排查起来非常费劲因为你在元素上看不到任何异常。6.4 vertical-align带来的基线对齐问题inline-block元素默认按基线对齐如果同一行的格子内容高度不同会出现高的格子把矮的格子顶下去一点的错位现象。修复方式是给格子加vertical-align: top让它们按顶部对齐。还有一个小坑是width: 33.33%的精度问题。三个33.33%加起来是99.99%理论上还剩0.01%的空间浏览器通常会把这个空间分配给某个格子但不同内核的处理可能不一致。稳妥的做法是用33.3333%或者直接用calc(100% / 3)。7. 五种方案的横向对比与实际选型建议7.1 按项目条件做选择的决策路径我一般的判断顺序是这样的需不需要支持很旧的浏览器内核如果是比如某些存量移动端项目那grid可能用不了退到flex或table。是不是二维等分且间距均匀是的话直接grid不用犹豫。最后一行是否需要居中或特殊对齐需要的话慎用flexgrid更合适。内容本身是表格数据那用table反而语义更好。只是维护老代码那就按它原来的方案改别顺手重构。7.2 一个容易被忽略的维度可访问性纯CSS布局对屏幕阅读器的影响其实不小。用display: table模拟的表格在无障碍树里不会变成真正的表格读屏软件读出来是一堆普通div。反过来如果你用真table标签做九宫格导航屏幕阅读器会把它当成数据表报第几行第几列这对导航类内容反而是噪音。所以结构语义和视觉布局应该分开看导航用ul加grid数据矩阵用table各归各位。这一点在设计阶段就该定下来后期改语义的代价比改样式大得多。7.3 关于渲染性能的一点实测感受三者对比下来在几十个格子这个量级上grid、flex、table的渲染差异几乎感知不到不用为性能纠结选型。真正影响性能的是别的东西大量盒阴影、滤镜、以及未优化的图片。九宫格最常见的性能问题是图片墙里塞了九张原图导致首屏加载缓慢这时候该做的是图片懒加载和尺寸裁切而不是去优化布局方式。8. 九宫格做完之后这些细节才是用户真正能感知到的8.1 点击区域和视觉区域不一致九宫格做导航的时候视觉上格子之间是有间隙的但点击区域最好覆盖整个格子包括间隙的一半。否则用户点在两个图标中间的空隙上没有任何反应会觉得这个按钮不灵。常见做法是给格子本身设置padding或使用伪元素扩展点击热区而不是靠间隙来分隔。同时要处理iOS上的点击高亮问题-webkit-tap-highlight-color的默认灰色方框在彩色图标上非常突兀通常要显式设成透明然后自己用:active状态给出反馈。8.2 图片纵横比和object-fit图片墙的格子通常是正方形但图片本身可能是各种比例。如果不约束图片会把格子撑变形。写法是.cell img { width: 100%; height: 100%; object-fit: cover; }object-fit: cover让图片按比例铺满并裁切多余部分contain则是完整显示但会留白。九宫格缩略图场景基本都用cover因为留白会让整面墙看起来参差不齐。8.3 边框和分隔线的处理方式如果九宫格需要分隔线别用每个格子加border的方式那样相邻格子之间会出现双线。替代方案有三种整体用gap留白不加线、用容器的背景色透过gap露出形成线、或者用:not(:nth-child(3n))给非末尾格子加单边线。第三种最灵活但也最容易在响应式改列数时忘记同步改除数3n要跟着列数变这是我踩过不止一次的坑。8.4 换行数变化时的hover态表现格子的hover效果背景变色、图标放大如果涉及尺寸变化比如transform: scale(1.05)在grid容器里可能会被相邻格子的层级盖住。解决办法是给hover的格子加position: relative; z-index: 1让它在视觉上浮到上层。最后再分享一个小技巧做九宫格的时候我喜欢先把容器宽度设成一个固定值比如375px开发所有间距和尺寸调舒服了再换成百分比或fr做自适应。这样能避免一开始就在响应式里绕圈子改一个值要到处验证效率高很多。
返回列表