
1. 写在前面从一次“卡片怎么都排不齐”的改版说起这两年做前台页面最常遇到的场景不是复杂的交互而是那种“看起来简单排起来要命”的卡片网格。尤其是后台看板、作品集、资讯流这类页面卡片尺寸不可能永远一模一样运营位要突出头条要加宽活动入口要占两行普通的列表卡片又只占一格。这个需求如果交给 Flex 去做通常就得靠flex-grow、flex-basis加一堆计算宽度的手工活再碰上内容长度不一致一行卡片的高矮差能把强迫症直接逼疯。换 CSS Grid 就顺理成章了特别是它自带的跨行跨列能力天生就是为“不规则但有序”的网格准备的。我最初接触 Grid 跨行跨列是在一套数据大屏的改造项目里。当时页面上有一堆图表卡片两个核心指标卡需要明显比普通卡大一圈剩下的是各种列表、趋势图、进度面板。用 Flex 硬排了几天每次数据刷新、卡片内容变化布局都会“跳一下”。后来狠下心全部切到 Grid用grid-row: span 2、grid-column: span 2以及grid-template-areas重写整个布局稳定得让人感动。这篇文章就把我实际踩过的坑、验证过的方案、以及后来给团队新人讲课时整理的一套方法论完整拆给你看。不管你是刚接触 CSS Grid还是已经用了一段时间但没系统研究过跨行跨列这篇都能给你一些能直接抄作业的东西。主要解决的就是一件事如何用 Grid 的跨行跨列搭建尺寸不一的卡片网格并且让它在不同屏幕下都不翻车。2. 基础不牢就会乱网格线、跨行跨列到底在跨什么2.1 先理解“网格线”而不是“网格框”很多人第一次接触 Grid 跨行跨列时习惯按“第几行第几列”来理解比如“这张卡片放在第 2 行第 3 列”。这个想法没有错但真正操作起来会栽跟头因为 Grid 定位依赖的不是格子grid cell而是网格线grid line。用一个生活化的类比Grid 容器就像一张多行多列的火车票座位表。你说“我要坐第 2 排第 3 座”这是按格子定位但 Grid 里的跨行跨列更像是“我要从第 2 条过道和第 3 条过道之间一直延伸到第 4 条过道”。你定位的始终是一条条的线。所以grid-column: 1 / 3的意思是“从第 1 条列线开始一直延伸到第 3 条列线”结果占了两列。同理grid-row: 2 / 4就是从第 2 条行线到第 4 条行线占两行。实际写代码时我喜欢用grid-column-start、grid-column-end的简写grid-column以及grid-row-start、grid-row-end的简写grid-row。举一个最朴素的例子.card--feature { grid-column: 1 / 3; grid-row: 1 / 3; }这段代码的意思就是把一张卡片放到网格的左上角并且跨两行两列。但这里有一个新手特别容易踩的坑grid-column: 1 / 3和grid-column: span 2在“视觉结果”上可能一样但在“自动排列”时表现完全不同。1 / 3是绝对定位这张卡片一定从第 1 条线开始span 2是相对跨度卡片只声明“我要占两列”至于从哪条线开始由 Grid 的自动排布算法决定。如果网格里还有其他卡片绝对定位和相对跨度的组合顺序不一样出来的排版结果能差出十万八千里。我见过新手用grid-column: 2 / 4把卡片硬塞到第二三列结果前面留了一个大空洞这就是没有吃透“线定位”带来的后果。2.2 一条span搞定最简单也最常用的跨行跨列如果你不需要精确控制卡片从哪条线开始只需要“这张卡片比旁边的大一倍”那么span关键词是最省心的写法。它让卡片按自身的尺寸需求去占用相应的轨道数量同时让 Grid 自动排列算法决定最终的位置。典型场景是 Bento 风格的卡片墙.grid { display: grid; grid-template-columns: repeat(4, 1fr); grid-auto-rows: 160px; gap: 16px; } .card--wide { grid-column: span 2; } .card--tall { grid-row: span 2; } .card--large { grid-column: span 2; grid-row: span 2; }这里repeat(4, 1fr)定义了 4 列等宽轨道grid-auto-rows: 160px设置了每一行的默认高度。之后 Grid 自动排列时遇到.card--wide就会占满 2 列遇到.card--tall就占满 2 行遇到.card--large就跨两行两列完全不冲突也不会被挤到容器外面去。为什么这里推荐grid-auto-rows而不是写死每一行高度因为卡片内容往往长短不一如果行高固定为 160px内容一旦超出就会出现我们常说的“溢出打架”。grid-auto-rows配合minmax()可以这样写.grid { grid-auto-rows: minmax(160px, auto); }这样每行至少 160px内容多了就自动长高。卡片里的文字、按钮就不会被挤压变形。这个细节我之前演示给同事看时大家普遍表示“早该这么写之前用固定高度害得我要给每个卡片算内边距”。2.3 用grid-template-areas做可视化布局适合整体框架跨行跨列还有一种更高级、也更直观的写法那就是grid-template-areas。它可以把网格变成一张“字符画”你直接用不同的命名区域表示不同卡片的位置和跨度一眼就能看出整体布局长什么样。比如我们想让页面上方有两张大卡片下面均匀分布 4 张小卡片可以这样写.grid { display: grid; grid-template-columns: repeat(4, 1fr); grid-template-areas: hero hero side1 side2 hero hero side3 side4 card1 card2 card3 card4; } .card--hero { grid-area: hero; } .card--side1 { grid-area: side1; }这段代码的核心就是grid-template-areas这个属性。它把 4 列网格当作一个 3 行 4 列的字符矩阵hero在矩阵中占了两个格子两行视觉上就是一张跨两行两列的大卡片。下面的side1、side2、side3、side4各自占据右上角的下半部分和右侧中间部分最底部一排四个卡片都只占一格。实际项目中我更推荐把grid-template-areas用在页面级别的整体骨架上比如一个看板页的顶部汇总区、中间图表区、底部列表区因为这种区域划分相对固定、改动频率低。而对于“卡片数量动态变化、内容来自接口”的列表型网格grid-template-areas写起来就费劲了每来一条新数据你不可能再去写一个grid-area的名称。这种情况下纯靠span加自动排列让 Grid 自己“见缝插针”反而更灵活。3. 动手写一个真实效果从静态到动态的混合尺寸卡片网格3.1 先搭出 12 列网格最灵活的基础容器做卡片网格时我习惯把列数定成 12 列而不是传统的 4 列或 6 列。12 这个数字称得上“布局界的万能胶水”因为 12 能被 2、3、4、6 整除想排两列、三列、四列、六列都方便。你要做跨两列、跨三列的卡片只要写上span 2、span 3剩下的轨道都能被其他普通卡片填满空隙率极低。一个标准容器可以这样定义.cards { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: 20px; grid-auto-rows: minmax(120px, auto); }这里我特意用了minmax(0, 1fr)而不是简单的1fr原因后面会在问题排查部分单说这里先记住一个结论在网格里做跨列卡片时minmax(0, 1fr)能避免内容过长把轨道撑爆从而让卡片宽度偏小的问题。此容器下普通卡片只占用一列.card { grid-column: span 1; grid-row: span 1; }其实普通卡片不写span也行Grid 会自动给它分配一格。但为了后续逻辑清晰我会显式写上这样别人看代码时一眼就能明白这张卡片是“普通尺寸”。大卡片就这么写.card--2col { grid-column: span 2; } .card--3col { grid-column: span 3; } .card--2row { grid-row: span 2; }当你把这些卡片摆进同一个网格容器时Grid 自动排列算法会根据卡片声明的大小寻找合适的位置放置它们。可能第一行排了 5 张普通卡加 1 张双宽卡第二行自动补位整个网格就像拼图一样被填满。3.2 用order属性打破排列僵局有些时候自动排列产生的结果虽然不空洞但卡片顺序不是我们想要的。比如某张海报卡片要求放在网格的第三位但它的宽度是普通卡的两倍直接按 DOM 顺序排下来可能它前面的一张普通卡填不满两个轨道导致海报卡要另起一行视觉上前面就缺了一块。这种情况下除了调整 DOM 顺序还可以用 Grid 的order属性。Grid 项目默认按 DOM 顺序排列通过设置不同的order值可以控制项目排序优先级值越小越靠前。还是以 4 列网格为例我们希望第三张卡片是双倍宽度div classgrid div classcard1/div div classcard2/div div classcard card--2col3 (双倍宽)/div div classcard4/div div classcard5/div div classcard6/div /div如果按正常顺序第 3 个元素是span 2它放着放着可能会把后面第 4、5 个元素挤到下一行造成第 4 个元素前面出现一个空洞。这时可以把第 3 个元素往前调.grid .card:nth-child(3) { order: -1; }它将双倍宽的卡片提到最前面。第一行3 号卡片占两列1 号、2 号各占一列刚好填满 4 列。第二行继续排 4、5、6 号整整齐齐。不过order也要慎用它只改变视觉排列不会改变 DOM 顺序对键盘阅读和屏幕阅读器的体验可能产生偏差。常规列表型卡片网格我还是建议直接在数据层排序把重点卡片前置这样更合理。3.3 配一张“Bento 风”经典示例我用上面这些基础能力组装了一个经典 Bento 风网格视觉上是 4 列底部高度适配内容区div classbento div classbento__item item-aA主推卡跨2行2列/div div classbento__item item-bB普通卡/div div classbento__item item-cC普通卡/div div classbento__item item-dD横向长条卡跨2列/div div classbento__item item-eE普通卡/div div classbento__item item-fF普通卡/div div classbento__item item-gG普通卡/div /div.bento { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); grid-auto-rows: minmax(140px, auto); gap: 16px; } .item-a { grid-column: span 2; grid-row: span 2; } .item-d { grid-column: span 2; }这套网格的效果是A 占据左上角四分之一块B、C 在 A 右侧上下排列D 横跨 A 下面两列E、F、G 继续在后面依次排列。卡片数量增加时普通卡片会自动填充剩余空间布局依然稳定。有人会问我为什么一定要把 A 放在最前。其实这是故意的因为 Grid 的自动排列默认从左到右、从上到下主推内容放前面能保证它在视觉上优先出现。如果你硬把 A 放在中间自动排列时它的span 2 / span 2可能会推着后面的卡片换行导致一堆空洞反而得不偿失。3.4 一个容易忽略的点grid-auto-flow: dense的“见缝插针”再回到动态数据场景。接口返回的卡片数据里可能会随机混有“双宽卡”“双高卡”如果只用默认的自动排列个别情况下会出现空洞一个双宽卡前面只剩一列空位它放不进去就换行到下一行留下了尴尬的空洞。在容器中加上grid-auto-flow: dense可以让后面的卡片往前填充空洞。打个比方默认排列就像坐高铁按票入座后面上来一个胖哥发现座位被前面的大包占了一半坐不下就只能换到下一排旁边留出空位dense模式则像演唱会散场后人们见缝插针看到空座先坐下再说。.bento { display: grid; grid-auto-flow: dense; /* 其余属性不变 */ }代价是它会改变某些卡片的视觉顺序比如 DOM 顺序靠后的卡片反而被提到了前面。如果卡片内容有强时间顺序、前后依赖关系dense要慎开。像“消息流”“时间线”这种场景顺序的重要性远高于填满空洞这时候宁可在接口层做数据预处理比如把大卡片尽量安插在小卡片序列后也不要开dense破坏时间线。4. 响应式场景下跨行跨列怎么不翻车4.1 断点设计从“列数”入手比从“卡片尺寸”入手更省心做跨行跨列的卡片网格时响应式设计的核心问题不是“卡片怎么变小”而是“网格还有几列”。列数一变跨行跨列的含义就全变了。我见过同事在移动端布局时给每张.card--large写了grid-column: span 2结果网格改用 2 列之后这张大卡片直接横跨整个屏幕看着特别突兀。所以我的建议是先决定当前屏宽下网格显示几列再决定跨列卡片的“跨度”是否需要跟着改。比如桌面端用 12 列网格平板上改 8 列手机上改 4 列。那么双横卡片span 2在桌面端只占 2/12 的宽度到移动端 4 列里占 2/4已经是一半宽度视觉上仍然合理。但如果一个原本占 6/12 的巨型卡片在移动端还占 6 列的话就只能把 4 列网格塞爆或者缩成一整行这显然不是我们想要的效果。一个典型的响应式方案.cards { display: grid; grid-template-columns: 1fr; gap: 16px; } media (min-width: 640px) { .cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } } media (min-width: 1024px) { .cards { grid-template-columns: repeat(4, minmax(0, 1fr)); } }对应的卡片跨度我一般不在每个断点里重复写span而是保证跨度在可用列数以内即可。比如.card--2col在双列下占 2 列就是整行在四列下占 2 列就是半行视觉上都说得过去。只有巨型卡片才需要特殊断点单独处理把它从span 2改成span 1。4.2 内容长度不一致时用align-items守住底线卡片网格最容易出的问题之一是同一行里的卡片高度不一致。Grid 默认情况下同一行的项目会被拉伸stretch到这个行轨道的最高高度这本来是好事因为整行高度整齐。但如果你在卡片内部用了height: 100%或者让背景色直接铺满卡片内容少的那张就会显得特别空。我常用的方案是卡片外层负责网格占位卡片内部再用一个flex容器承载内容让内容区自然伸缩。这样可以避免 Grid 的拉伸效果干扰内容排版。div classcard div classcard__inner h3标题/h3 p内容描述/p /div /div.card { display: flex; flex-direction: column; } .card__inner { display: flex; flex-direction: column; flex: 1; }这样即使一张卡片内容很多、另一张内容很少两者的底部也能对齐。Grid 管理的是“这张卡片占哪些行和列”Flex 管理的是“卡片内部内容怎么排”两者分工明确互不打架。4.3 自动填充轨道和最小卡片宽度卡片在流式布局中最怕的就是内容一多轨道被撑得乱七八糟。repeat(auto-fill, ...)是常用于栅格列表的高级写法可以自动根据容器宽度生成尽可能多的轨道但一旦跟span组合却容易出问题。举个例子.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 16px; } .card--2col { grid-column: span 2; }这种写法在桌面端很理想容器宽时能生成很多轨道双宽卡片自动占两个轨道。但到窄屏时如果minmax(200px, 1fr)的最小值偏大容器宽度又不足 400px双宽卡片可能会直接撑出容器触发横向滚动条。解决办法是调小minmax的最小值或者在不同断点下改grid-template-columns而不是依赖一个万能值。我从实践中得到的结论是如果卡片数量相对固定用repeat(4, 1fr)这类显式列数如果卡片数量动态变化用repeat(auto-fill, minmax(...))并且一定给容器加上min-width: 0避免溢出。5. 热心网友经常搜的伪类、删除线、精灵图这些其实都能“点缀”卡片网格标题里提到的热搜词里有一堆看起来和跨行跨列无关的 CSS 小技能比如css伪类选择器怎么用、css 删除线、css 鼠标移入事件、css中怎么用ps制作精灵图。这类关键词恰恰是“卡片网格”提升质感的一环。我干脆把它们串起来教你如何在一个已经搭好跨行跨列结构的网格里用这些小技巧提升卡片的表现力。5.1 用:hover和伪类选择器做卡片交互卡片网格里最常见的交互是鼠标悬停。:hover本身不复杂关键是要选对选择器否则可能会出现“鼠标移到卡片空白处没有反馈”的尴尬情况。我通常把悬停效果写在卡片整体上而不是卡片里的某个文字上。给卡片加上边框变色、阴影加深、轻微上浮三个效果组合起来就很自然.card { transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; border: 1px solid #e5e7eb; border-radius: 12px; } .card:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.08); border-color: #6366f1; }伪类选择器还有一个实用场景给第一张卡片或最后一张卡片做特殊处理。比如你希望第一张大卡片作为“本期主打”可以这样写.card:first-child { background: linear-gradient(135deg, #4f46e5, #7c3aed); color: #fff; }这是 CSS 里面最基础也是最好用的能力。nth-child、:not()、:first-child、:last-child熟练之后基本可以覆盖卡片网格 90% 的特殊场景。5.2 优惠券圆切、删除线、糯米字体渐变这些小技巧怎么融进去有些 CSS 玩法一看就是做卡片必备的优惠券圆切通常是利用mask或radial-gradient在卡片边缘切出半圆缺口。做法是用两层背景叠加出“齿轮状”边缘或者直接用mask抠图。删除线原价、划线价这类信息在电商卡片里几乎到处都是。text-decoration: line-through是最简单的方式。如果想让删除线更细、颜色更浅可以增加text-decoration-color和text-decoration-thickness两个属性。字体渐变给数字或标题做渐变文字方式也很简单background: linear-gradient(...)配合-webkit-background-clip: text和color: transparent就能实现。精灵图它把多个小图标合成一张大图再用background-position定位到具体位置。卡片网格中最适合放精灵图的地方是图标列表、状态标识区。这些都是搭配卡片网格的好帮手单独拿出来讲可能一篇都要写几千字但在卡片网格语境下他们的角色就是“让卡片更精致”的调味料。5.3 鼠标移入事件与 JavaScript 的边界大家搜索“css 鼠标移入事件”我猜很多人其实是想问“鼠标移入卡片时怎么让其他卡片联动变化”。用纯 CSS 也不是不行但能力有限。比如当鼠标悬停在网格上时让所有非悬停卡片降低透明度.grid:hover .card { opacity: 0.6; } .grid:hover .card:hover { opacity: 1; }这个效果在卡片墙里非常能提升高级感。如果你需要更复杂的联动比如“鼠标悬停大卡片时右侧小卡片说一句话”那还是交给 JavaScript。CSS 能做的交互尽量在 CSS 层解决省去事件监听的性能开销CSS 搞不定的果断上 JS不要硬刚。5.4 说一个常见的搜索迷思flex:1为什么还剩一点宽度热搜词里有一条特别有意思“css display: flex; 子级 flex:1; 为什么最后还剩一点宽度”。这个问题经常出现在 Flex 布局的卡片内部和 Grid 的跨行跨列也有关系。简单说flex: 1是flex-grow: 1; flex-shrink: 1; flex-basis: 0%的简写它表示项目在主轴方向上有剩余空间时按比例扩展。如果还剩一点宽度通常是因为子元素里有内容自带min-width比如图片、长单词、内联元素它阻止了项目压缩到 0 基准。解决办法是在子元素上加min-width: 0或者overflow: hidden。放在 Grid 卡片里这个问题更容易变成卡片内部用了display: flex然后某块区域flex: 1却没有撑满卡片宽度。这种情况下除了min-width: 0还要检查卡片本身有没有width: 100%或者align-self属性被覆盖。6. 常见问题速查表与 Chrome 调试备忘6.1 我实际踩过的“疑难杂症”现象可能原因解决方案卡片跨列后把其他卡片挤到下一行出现空洞自动排列算法找不到合适位置调整 DOM 顺序、加grid-auto-flow: dense卡片内容很长时跨列卡片宽度不对轨道被内容撑开1fr失效用minmax(0, 1fr)替代1fr大卡片没有缩放直接溢出容器没设置min-width: 0给 Grid 项目加min-width: 0不同断点下大卡片比例失真跨列跨度没跟着列数变化在媒体查询里重设grid-columngrid-template-areas区域名称对不齐布局崩坏字符数量或形状不一致确保每个区域的 ASCII 图形行列数严格一致卡片高度参差不齐行内出现白条内部内容撑不起网格行高使用grid-auto-rows: minmax(120px, auto)内部用 Flex 填充双高卡片在底部留下一截空白行高由内容决定双高卡内容太少配合grid-auto-rows固定最小高度再用align-content调整幽灵空白出现在某个卡片旁边order或dense改变了排列顺序检查是否开了dense必要时关闭使用repeat(auto-fill)时卡片宽度忽大忽小轨道数量随容器宽度变化跨度固定改用显式列数或限制容器宽度6.2 Chrome DevTools 里的网格调试技巧Chrome DevTools 的 Grid 调试功能我已经用了很久每次排卡片网格都要打开。操作很简单在 Elements 面板选中网格容器Styles 面板里网格相关属性旁边会出现一个网格图标点一下就能看到行号、列号和网格线编号。这对调试跨行跨列特别有用因为“从第几列到第几列”不再需要靠猜屏幕上直接标出数字。另一个实用技巧是在 Elements 面板里选中某个卡片元素在 Layout 面板中勾选Show line names和Show track sizes立刻能看到它在网格中的起止行列线以及该区域具体像素尺寸。遇到“卡片为什么占了两行”的问题打开这个面板基本一眼就知道原因是grid-row写错了、还是父容器grid-auto-rows给了固定高度。6.3 调试三连检查容器属性、检查项目属性、检查轨道尺寸我调试自己的卡片网格时有一套固定流程像体检一样按顺序排除问题第一步检查容器。display: grid是否生效grid-template-columns和grid-template-rows的数值是否符合预期。特别是grid-template-columns: repeat(4, 1fr)和repeat(4, minmax(0, 1fr))在 DevTools 里看起来差不多但实际布局差异很大。第二步检查项目。选中有问题的卡片看它的grid-column、grid-row到底是怎么写的。我经常发现有些人写grid-column: span 4但容器只有 3 列浏览器会自动把 span 约束到最大可用列数却不会报错这会让卡片尺寸变得很怪异。第三步检查轨道尺寸。打开 DevTools 的 Layout 面板点击 element 旁边的网格 overlay就能看到每条轨道具体宽高。用这个判断到底是不是轨道本身不够宽还是卡片内部内容把布局撑乱。两步一对照问题基本都藏不住。7. 卡片网格后续还能往哪个方向玩写到这里跨行跨列的核心用法已经都覆盖了。但卡片网格这个方向还有很多延伸玩法比如嵌套网格在一个跨两行两列的容器内部再做一个display: grid的子网格把多个小组件塞进同一张大卡片里。这种手法在数据大屏和官网首页非常常见比如一张大卡片内部分三列展示不同的统计指标。还有一点Grid 的grid-template-columns支持subgrid。子网格可以继承父网格的行列线在多级嵌套卡片里做对齐时特别好用。只是subgrid的浏览器兼容性现在虽已逐步完善用它时还是要根据目标用户群做判断。如果你做的是内部后台系统可以放心用如果是面向 C 端的官网建议先看浏览器占比再决定。我个人在实际操作中最深的体会是跨行跨列不是“写几行 CSS 就完事”的事它是布局思路的转变。用 Flex 的时候我的大脑是按“一横行接一横行”思考的切到 Grid 之后我开始把页面想象成一个矩阵哪张卡占几行几列哪个区域留白全都提前在图纸上想清楚。代码反而是最后一步。先把每条线画明白写出来的网格布局自然干净、稳定、好维护。