
先说我踩过的一个坑。早年间做数据大屏产品扔过来一张设计稿左上角一张大卡片占两列两行右边两张小卡竖着排下面还要穿插三张横向卡。我用flex硬拼套了三层容器又加margin负值最后在某个分辨率下全挤成一团。后来换CSS Grid的跨行跨列半小时搞定代码量砍了一半。Grid这个属性真正厉害的地方不是能排等宽卡片——那是它最基础的功能——而是它允许你明确告诉某个卡片你占几行几列这才是做不同尺寸卡片网格的核心能力。这篇文章就把Grid跨行跨列这套东西拆开讲透从原理、写法到实战和踩坑给你一份可以直接照抄的方案。1. 等宽网格和不同尺寸网格到底差在哪里1.1 从最熟悉的等宽网格说起大多数前端对Grid的认知停留在这一段.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }三个等宽列每行三张卡卡片高度由内容撑开。这解决的是均匀排列的问题而且它有个隐含假设所有卡片长得差不多、优先级一样、大小一样。但真实业务里这样的页面太少了。我用它做过一次后台面板第一版所有卡片等宽被产品和设计同时打回。原因很简单核心数据卡片比如总访问量、转化率需要更大的展示区域次要指标用小卡片就行文章列表里的置顶内容需要横跨整行普通内容占一格就够了。用flex实现这种布局要么嵌套一大堆容器要么在每个卡片上用flex-basis和width算百分比遇到多行多列的组合就直接失控。问题不在于flex不好而在于flex是在一维方向上排列它不知道行和列同时存在时的空间关系。Grid是二维布局系统天生知道网格的行列结构所以某张卡片占据多个格子对Grid来说是设计内的功能对flex来说是要靠hack绕路的功能。1.2 不同尺寸卡片的三种常见形态我把它归纳成三种需求基本覆盖了日常开发会遇到的情况需求类型视觉表现核心实现跨列不跨行一张卡占两列宽高度和其他小卡相同grid-column: span 2;跨行不跨列一张卡占两行高宽度与其他卡相同grid-row: span 2;既跨列又跨行大宫格卡片占两列两行同时写grid-column和grid-row理解这一点很关键。很多人以为不同尺寸就是要靠内容撑开、靠宽度百分比计算其实在Grid体系里尺寸是用占据多少网格单元来描述的。就像搭积木先画出等大的格子然后让某些积木跨过两个格子视觉上就成了大积木。1.3 为什么不用JS瀑布流或CSS columns可能有人会问现在瀑布流库那么多或者用CSScolumns属性是不是更简单我试过各有各的代价。JS瀑布流方案比如Masonry库需要监听图片加载、窗口resize动态计算每个卡片的绝对定位数据一变就要重新排性能开销不小而且和页面里的其他布局交互容易出问题。CSScolumns方案实现竖向瀑布流确实方便但它的排列顺序是自上而下、自左而右DOM顺序会被打乱而且它没法精确控制某一张卡横跨两列这种需求。Grid跨行跨列用的是原生排列算法卡片顺序可控无需额外库性能报表上基本没有负担这对我来说是决定性的优势。所以结论很明确静态或半静态的内容卡片网格Grid跨行跨列就是最优解。动态数据、需要流式插入的场景配合一点JS控制样式类也完全够用。2. 跨行跨列的核心写法grid-column与grid-row的三种用法2.1 span关键字不关心具体位置只说占几格最常见、最不容易出错的写法是span.card--wide { grid-column: span 2; } .card--tall { grid-row: span 2; } .card--large { grid-column: span 2; grid-row: span 2; }意思是当前卡片横跨两个列轨道竖跨两个行轨道两者都要。这种写法适合动态列表因为我不需要知道卡片排在第几行第几列只需要通过一个类名告诉浏览器它的尺寸偏好剩下的位置计算完全交给Grid自动排列算法。实际开发中我几乎都用这种写法。它最大的价值是防错不管网格前面有没有别的卡片、占没占位span 2就只是一个我要两格的声明浏览器会尽量满足。2.2 line-based写法用网格线序号精确控制第二种写法是用网格线的序号。要理解它得先记住一句口诀网格线数量 轨道数量 1。三列网格有4条竖线两行网格有3条横线。所以.card--hero { grid-column: 1 / 3; /* 从第1条竖线到第3条竖线占据第1、第2列 */ grid-row: 1 / 3; /* 从第1条横线到第3条横线占据第1、第2行 */ }新手常见的误解是把1 / 3理解成从第1列到第3列实际上是从线1到线3只占两格。我自己早年就因为这个理解错位做出来的卡片总比预期宽一列。line-based写法的好处是精确可控适合固定位置的卡片比如页面顶部的英雄卡我必须确保它在左上角。缺点是维护性略差一旦前面某个卡片改了跨度后续线号全要重新数。所以我一般只在结构稳定的骨架布局里用它。2.3 grid-area配合grid-template-areas把布局当图画第三种是最适合整体页面骨架的写法用命名区域来做可视化排布.grid { display: grid; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 100px 100px 100px; grid-template-areas: hero hero side hero hero side content content side; } .card--hero { grid-area: hero; } .card--side { grid-area: side; } .card--content { grid-area: content; }看一眼grid-template-areas的字符串就能脑补出页面长什么样左上角一个横跨两列两行的hero区右上角一个竖跨三行的侧栏底部一个横跨两列的内容区。这种写法特别适合做页面级别的骨架网格设计稿一改直接调整ASCII拼图不需要去算任何线号。grid-template-areas有几个规则需要注意每个单元格都要有名字用.表示空单元格两个区域只有形状是矩形才能命名L形或T形区域无法直接表达需要拆成多个小矩形。我在实际项目里如果遇到右上角卡片下面还要塞一个小卡片这类复杂形状会先用areas把大区域定好再用grid-column/grid-row微调局部卡片。3. 实战搭建一套可复用的不同尺寸卡片网格3.1 拼一个真实的页面骨架假设我们要做一屏数据分析面板布局需求是核心指标卡左上角横跨两列、竖跨两行侧边栏右上角竖跨两行普通卡片下方三张小卡HTML结构如下div classgrid div classcard card--hero核心指标/div div classcard card--side侧边列表/div div classcard普通卡片1/div div classcard普通卡片2/div div classcard普通卡片3/div /divCSS.grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-auto-rows: 120px; gap: 16px; } .card { background: #1e293b; border-radius: 12px; padding: 20px; } .card--hero { grid-column: span 2; grid-row: span 2; } .card--side { grid-row: span 2; }这里有个容易被忽略的关键点grid-auto-rows: 120px。因为我没有显式定义行而是让卡片通过span自动占据行这些行属于隐式网格行高默认是auto也就是由内容撑开。如果内容是空的或者很矮行高会很小跨两行的效果就看不出来。设置统一的行高后每一行都是120px两次跨越就是240px加上gap整体高度完全可控。3.2 用minmax auto-fill让列数自己适应宽度写固定三列的代码在业务里很容易翻车特别是同一套页面要跑在不同尺寸的屏幕上。我的做法是配合auto-fill和minmax.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); grid-auto-rows: 120px; gap: 16px; }这样网格会先尝试放尽可能多的列每列最小220px如果宽度够就放大。本意是好的但一旦配合span 2就会产生微妙问题在大屏上容器宽度是1000px每列约220px有4列span 2的卡片跨两列约456px到中屏宽度变成700px每列还是220px有3列span 2的卡片宽度就变成了456px左右视觉差异不大但如果minmax的下限设得过高列数从4掉到3跨度相差就会变得明显。所以更稳妥的做法是响应式场景中不要依赖自动列数配合大跨度。我用的是移动端不跨、平板跨两列、桌面跨两列两行这种分档策略每一档用媒体查询显式确定列数和跨度。后面第6部分会具体展开。3.3 大卡片和小卡片的内部排版细节跨行跨列的卡片视觉面积大内部排版如果不处理反而显得空洞。我总结了一个基本公式大卡片用大字号、大间距、大内边距。.card--hero { display: flex; flex-direction: column; justify-content: space-between; padding: 28px; } .card--hero .value { font-size: 40px; font-weight: 700; line-height: 1.2; } .card--side { display: flex; flex-direction: column; gap: 12px; }hero卡把数据值放中间、辅助信息放下方利用纵向空间制造对比感小卡片内容少保持紧凑即可。如果你卡片里有图片别忘了给图片容器设overflow: hidden给img设width: 100%; height: 100%; object-fit: cover;否则图片会被拉伸变形——这个问题在第5部分还会细说。4. 自动排列的补位逻辑grid-auto-flow: dense4.1 不做处理时网格会出现空洞当网格里同时存在跨行跨列卡片和普通卡片默认的自动排列算法是按DOM顺序从左到右、从上到下挨个放。看起来挺自然但一旦某个卡片因为跨行把位置占掉后面的卡片就会被挤到下一行去留下一个视觉空洞。举个例子网格3列第1张卡片是grid-column: span 2第2张卡片是普通卡片。排列时第1张卡片占据第1行的第1、2列第2张卡片放在第1行的第3列这部分没问题。但如果第1张卡片是grid-row: span 2它占的是第1行整行或者被行跨影响第2、3张卡片就可能被推到第2行而第1行剩下位置空着。空洞在视觉上很丑特别是卡片高度统一、背景色一致的时候一眼就能看出有一格是空的。这个问题不是bug而是Grid默认流式排列的正常行为——它不会主动把后面的卡片往前推去填空。4.2 dense为什么能填洞代价是什么解决方案是给网格容器加一行.grid { display: grid; grid-auto-flow: dense; }dense的意思是回溯填充当某一行有空格时允许算法跳过按DOM顺序该排的卡片从后面拉一张尺寸合适的卡片过来填空。一张原本要放在第3行的普通小卡片会被提前到第1、2行的空洞里整体画面会非常饱满。代价是DOM顺序和视觉顺序不再一致。屏幕阅读器、搜索引擎抓取、键盘Tab导航都是按照DOM顺序走的如果你把视觉上排在第1位的卡片在DOM里放到第5位用户用读屏软件听到的顺序就和看到的完全对不上。这在新闻流、表单类卡片里是严重的可用性问题。我的建议是分场景对待图片墙、作品集、Dashboard面板内容之间的逻辑顺序不强可以用dense视觉效果优先新闻列表、活动报名、流程步骤顺序本身就是信息的一部分别用dense宁可接受少量空洞或者手动调整DOM顺序。另外dense对跨行跨列特别有用。当所有卡片只有普通尺寸时自动排列很少产生空洞跨行跨列卡片越多空洞概率越大dense的作用也越明显。所以两个功能经常是配套使用的。5. 踩坑实录跨行跨列布局最常见的5个问题5.1 行高不一致卡片根部对不齐这是我最常被问的问题。有人设置了grid-template-columns但没设置grid-auto-rows然后给某张卡写了grid-row: span 2出来的效果是这张卡确实比旁边卡片高但高多少完全由内容决定导致同行的其他卡片也被撑得高度参差不齐。解决方案就是之前提到的显式声明隐式行的高度。.grid { grid-auto-rows: 100px; }如果需要行高随内容增长用minmax.grid { grid-auto-rows: minmax(100px, auto); }这里auto允许内容超过100px时自动撑高但所有行保持至少100px的基本高度。这个写法在处理卡片高度自适应但至少占一格高度的需求时非常好用。5.2 repeat(auto-fill) span 2 的列数冲突这个问题在3.2里提过这里再展开一下。repeat(auto-fill, minmax(240px, 1fr))的列数是响应式变化的在1200px和800px的屏幕上实际列数可能不同。这时一个span 2的卡片在不同断点下跨的列数一样但每一列的宽度不同所以大卡片的绝对宽度会有明显跳变。更麻烦的是如果某一档屏幕下span 2的卡片加上它的跨距超出了可见区域卡片会被挤到下一行产生意外换行。逻辑上span 2不会超过容器宽度因为2列之和一定小于等于总列数但在auto-fill下总列数会动态变化极端情况下卡片跨了最后两列然后被wrap到新行留下大片空白。我的做法很直接在需要跨行跨列的容器上不要用auto-fill改成固定列数然后用媒体查询调整固定值。.grid { display: grid; grid-template-columns: repeat(4, 1fr); grid-auto-rows: 120px; } media (max-width: 900px) { .grid { grid-template-columns: repeat(2, 1fr); } }固定列数配合媒体查询跨度的行为完全可预测。5.3 把1 / 3误当成第1列到第3列前面提过line-based容易搞错。我见过不止一次有人想做一个横跨两列的卡片写的是grid-column: 1 / 2结果只占了一列还有人想从第1列跨到第3列写grid-column: 1 / 4结果占了整整三列。记住grid-column: start / end两端的数字是网格线的编号不是列号。列1位于线1和线2之间列2位于线2和线3之间。所以要占第1列和第2列应该写1 / 3。如果你记不住就统一用spangrid-column: span 2;这个写法永远不出错因为你不关心起止线只关心占几个轨道。5.4 大卡片里的图片被拉伸跨两列的卡片宽度加倍如果直接放一张普通尺寸的img要么宽度100%导致高度失真要么高度100%导致宽度失真。Grid只管分布单元格位置不会自动帮你裁剪图片。标准解法.card--hero { position: relative; overflow: hidden; } .card--hero img { width: 100%; height: 100%; object-fit: cover; position: absolute; top: 0; left: 0; }object-fit: cover会保持图片比例同时填满容器超出部分裁剪掉。用absolute而非普通流内图片是为了避免图片把卡片高度撑爆特别是卡片本身已经有grid-row: span时内部子元素的高度必须受限于网格轨道高度。5.5 grid-area简写顺序和兼容性grid-area用于简写时顺序是grid-row-start / grid-column-start / grid-row-end / grid-column-end也就是先行后列。这个顺序和CSS其他方向简写上右下左完全不同非常反直觉。比如grid-area: 1 / 2 / 3 / 4;意思是从第1行线、第2列线开始到第3行线、第4列线结束占的是第1行到第2行、第2列到第3列。不熟的时候很容易搞混。建议日常不要用这个简写除非你在使用grid-template-areasgrid-area: 区域名的命名写法后者简单清晰。兼容性方面现代浏览器对Grid的跨行跨列支持已经非常完善Chrome、Firefox、Safari、Edge都没有问题。老项目如果需要兼容IE11情况会比较麻烦IE用的是-ms-grid-column和-ms-grid-row而且不支持span关键字只能写-ms-grid-column: 1; -ms-grid-column-span: 2;这套老语法。如果团队维护的是需要兼容IE的旧系统跨行跨列的成本会高很多。新项目基本可以忽略这个顾虑。6. 从Demo到工程化响应式策略与动态数据配合6.1 移动端优先的跨度控制跨行跨列的卡片在窄屏上很容易失衡一张占两列的大卡片在手机端会把旁边的小卡片挤得只有半个屏幕宽惨不忍睹。所以我的策略是默认禁用跨度到中等屏幕再逐步开启。.grid { display: grid; grid-template-columns: 1fr; gap: 16px; } /* 平板两列允许跨列 */ media (min-width: 640px) { .grid { grid-template-columns: repeat(2, 1fr); } .card--wide { grid-column: span 2; } } /* 桌面四列允许跨行跨列 */ media (min-width: 1024px) { .grid { grid-template-columns: repeat(4, 1fr); grid-auto-rows: 120px; } .card--big { grid-column: span 2; grid-row: span 2; } }这样手机端每一张卡片都是独立一行阅读体验干净平板上可以出现一张横卡两张竖卡的组合桌面端才展示完整的Bento风格。移动端优先的最大好处是用户不管从哪种屏幕进来都不会看到被压缩到没法看的卡片。6.2 动态列表数据怎么安全地套跨度如果卡片列表是由接口数据渲染的没法像静态页面一样手工给每张卡指定位置。我的做法是在渲染时按照数据的索引或类型给特定项追加跨度类。比如一个内容信息流我希望每5项里第1项是横跨两列的大卡list.map((item, index) { const className index % 5 0 ? card card--wide : card; return div class${className}${item.title}/div; }).join();但这有个风险如果第5项不在某一行的行首而是落在中间列span 2会让它挤出容器。所以使用这种动态方案时页面的列数必须是固定的且跨度卡片最好固定占一行。像上面repeat(4, 1fr)四列的情况下span 2的大卡一定是成对出现的通常不会出问题。如果是repeat(3, 1fr)每张span 2的卡旁边必须紧跟一张普通卡否则就会换行并可能产生空洞。这一点在写渲染逻辑时就要考虑清楚。6.3 subgrid跨行跨列布局的进阶能力如果项目不需要兼容老浏览器值得关注一下subgrid。它解决的核心问题是子元素的内部网格要能和外部网格对齐。举个例子外层大卡占了两列两行内部要放两个纵向区块如果不做特殊处理内部的两个区块只能靠flex均分无法精确对齐外层其他卡片的列线。用subgrid可以让子网格继承父网格的轨道定义.card--hero { display: grid; grid-template-columns: subgrid; grid-row: span 2; }此时卡片内部的列轨道与外层网格的列轨道完全一致内部元素就可以按照外层列线来排布。这在做复杂嵌套卡片的时候特别有用能避免很多因为内外网格宽度不一致导致的像素级错位。需要注意subgrid目前在Firefox和Chrome较新版本支持Safari 16之后也支持覆盖率已经可以接受但在正式项目里还是要确认用户群体的浏览器版本分布。从纯等宽网格切换到跨行跨列布局是我觉得CSS Grid最值得学的一层能力。它不依赖任何框架不引入任何JS原生就能撑起复杂的卡片页面。如果你现在要做Dashboard面板、作品集展示、数据大屏或者各种Bento风格的页面建议直接在Grid容器上把grid-auto-rows固定下来然后用span给特定卡片声明跨度再按需开dense。这套组合拳基本能覆盖我工作中遇到的大部分场景。顺手分享一个我自己的习惯跨行跨列最多控制在两栅格以内也就是span 2或span 3就收手。真正在业务里需要span 5、span 6的场景少之又少而且视觉上很容易失衡用户需要在大片色块里找焦点体验并不好。网格布局的价值在于秩序感克制使用跨度反而能让大卡片更有冲击力。