### [响应式无缝网格布局:auto-fit + minmax() 之外,真正会崩的几个地方](https://www.huociguo.com/article/1442) **Published:** 2026-09-26T09:36:32 **Author:** 米了 **Excerpt:** 上个月接手一个电商老项目,首页商品墙在 iPhone 上一列、iPad 上两列、27 寸显示器上——居然还是两… 上个月接手一个电商老项目,首页商品墙在 iPhone 上一列、iPad 上两列、27 寸显示器上——居然还是两列,右边空出老大一块白。同事挺委屈:”CSS 明明写的是响应式啊。”我打开一看,`grid-template-columns: repeat(auto-fit, minmax(300px, 1fr))` 一字不差。 这行代码确实没错,错的是它周围那一圈东西:视口标签、盒模型、图片尺寸、hover 动画,任何一处没管住,网格就会以各种姿势崩给你看。下面按我实际排查的顺序写,都是真踩过的坑。 ## **viewport 没写,后面全白干** 排查那个商品墙时我第一件事是看 `
`,果然没写 viewport。这是最容易被忽略的一条,因为它在电脑上看不出任何问题——你在 Chrome 里拖窗口,网格照样老老实实地变列数,一切正常,于是你以为代码是对的。 但手机浏览器不按你想的来。缺了这个标签,它会拿一个约 **980px** 的虚拟视口去渲染整页,再整体缩小塞进屏幕。结果就是:`@media` 的断点压根不触发,`auto-fit` 算出来的列数也是基于 980px 算的。你在真机上看到的”两列”,其实是 980px 宽下算出的两列被缩到了屏幕大小。 ``` ``` 就这一行。它决定了后面所有响应式逻辑有没有机会执行。 **别加** `user-scalable=no`**。**它禁掉了双指缩放,属于无障碍红线。有些安卓 WebView 对 `initial-scale=1` 解析不准,可以补 `maximum-scale=1` 兜底,但 pinch-zoom 一定要留着。 ## **minmax() 的上限,只能是 1fr** 回到那行 CSS。很多人抄的时候会”顺手优化”成这样: ``` /* 别这么写 */ .grid { grid-template-columns: repeat(auto-fit, minmax(280px, 300px)); } ``` 看起来很合理:每列最窄 280px、最宽 300px,多工整。但在 500px 宽的容器里就露馅了——两列要 560px,塞不下;退成一列,可这一列又被 300px 的上限锁死,右边剩下 200px 全是空白。上限写死像素,等于把弹性交还给了你自己,而你自己算不准。 `1fr` 的意思是”有剩的就均分”,它不只分配余量,也是小屏下自动退化成单列占满的关键信号。所以正确写法里,上限永远是 `1fr`: ``` .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px; } ``` 240px 这个数不是拍脑袋来的。iPhone SE 视口宽 375px,减掉页面左右留白后,内容区通常在 340px 上下,单列时 240px 的下限不会溢出,也撑得住一句正常的卡片标题。内容比较密的卡片可以往上抬到 280px 或 320px,但别再低了——低于 220px 时,中文标题基本每行都在换行。  同一套网格在不同视口宽度下的列数变化:容器够宽就多开一列,窄了就收,全程不需要媒体查询 ### **auto-fit 还是 auto-fill,选错了右侧就是一片白** 这两个关键字的区别只有一句:**auto-fit 会把空轨道合并掉,auto-fill 会把空轨道留着**。 卡片流、画廊、商品墙这类”有多少内容显示多少”的场景,一律用 `auto-fit`。合并空轨道之后,剩下的三张卡片会拉伸填满整行——这才是”无缝”的来源。换成 `auto-fill`,就算只有一张图,右边也会虚位以待地留出七八个空格子,视觉上就是右边塌了一块。 `auto-fill` 的用武之地是槽位固定的东西:九宫格、仪表盘占位、日历。想验证自己用的是哪个,Chrome DevTools 里切到 Layout 面板,勾上 _Show line numbers_,轨道号直接画在页面上,被合并的一目了然。 ### **更保险的写法:给下限也加个上限** 还有一个只在极端窄屏出现的坑:容器比 240px 还窄(比如折叠屏外屏、或者卡片被塞进了窄侧边栏),这时下限 240px 会强行撑出横向滚动条。加一层 `min()` 就能堵住: ``` .grid { grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); } ``` min(100%, 240px) 表示”下限取容器宽度和 240px 里更小的那个”,容器再窄也不会溢出。 ## **gap 和 box-sizing:两个不出声的守门员** 网格间距用 `gap`,别用子项 `margin` 去凑。原因不是”更优雅”,而是两者参与计算的时机不同:`gap` 是轨道之间的间距,发生在 Grid 算完列宽之后;`margin` 算在盒模型里,会先吃掉一部分宽度,`minmax()` 拿到的可用空间就少了,列数可能因此少一列。 用 margin 还有个经典后遗症:为了清掉每行最后一个的右边距,你得写 `:nth-child(3n)` 之类的选择器。可列数是浏览器动态决定的,3n 在两列布局下立刻错位,最后一行必然对不齐。 ``` .grid { display: grid; gap: 16px; } /* 对:间距只写一次 */ .card { margin: 8px; } /* 错:参与盒模型,挤压轨道计算 */ ``` 另外 `gap` 别全站统一写一个值。手机上 12px 就够,桌面 24px 才显得透气,用 `clamp()` 一句话搞定,省掉一个断点: ``` .grid { gap: clamp(12px, 2vw, 24px); } ``` **盒模型别忘了统一。**没写 `box-sizing: border-box` 时,卡片的 `padding` 和 `border` 是往外长的,实际占宽会超过 Grid 分配的轨道宽度。列宽计算从第一步就偏了,后面怎么调都不对。全局重置最省事:`*, *::before, *::after { box-sizing: border-box; }` ## **Grid 只负责划线,内容撑破它不管** 这是新手最容易困惑的地方:明明列宽定得好好的,一张图就把整行撑歪了。因为 Grid 分配的是轨道,轨道里的元素自己要多大,它默认不拦。 图片必须显式约束。`max-width: 100%` 只解决横向,纵向还得靠 `height: auto` 保住比例;如果卡片高度要统一,用 `aspect-ratio` 锁比例比写死 `height` 可靠得多,配 `object-fit: cover` 裁切,图片不会变形: ``` .card img { display: block; width: 100%; height: auto; aspect-ratio: 4 / 3; object-fit: cover; } ``` 文字这边主要防长串。URL、连续英文、没空格的订单号,都能把卡片顶宽。另外 Grid 子项有个”自动最小尺寸”:它的最小宽默认是内容宽,而不是 0。所以碰到长内容时,要显式允许它被压缩: ``` .card { min-width: 0; /* 允许子项被压缩,而不是顶开轨道 */ overflow-wrap: anywhere; /* 长串强制断行 */ } ``` ## **hover 时下方卡片”弹一下”,跟 transform 没关系** 这个现象我见过太多次:鼠标移上去,下面一排卡片跟着抖一下。第一反应都是改 `transform: scale()`,改完没用——因为真正的原因是**卡片高度变了,引发了整行重排**。 最常见的触发方式,是 hover 时给卡片加了 `border` 或者把 `box-shadow` 从”无”变成”有”。尺寸一变,行高一变,后面的行全跟着动。 治法是对症下药: - 给卡片一个 `min-height` 锚定基线。它比写死 `height` 灵活,内容多时还能往下长。 - 边框别在 hover 时出现。想有描边效果,平时就写上透明边框 `border: 1px solid transparent`,hover 只改颜色。 - 动画只碰 `transform` 和 `opacity`。这两个走合成层,不触发重排;改 `width`、`margin`、`top` 就一定重排。 - 阴影用 `filter: drop-shadow()` 或常驻的 `box-shadow` 改透明度,别凭空新增。 - 用了 `grid-auto-rows` 时更要小心,隐式行高会跟着内容走,一个卡片变高,后续所有行都位移。必要时改用显式 `grid-template-rows`。 `will-change: transform` **别全局加。**它确实能让动画更顺,但每个元素都会提前占一个合成层,卡片一多内存就上去了。只给确定会频繁交互的那几个加。 ## **IE11 和旧 Safari 的兜底** 如果项目还要管 IE11(希望不用,但确实还有),策略是别硬扛,直接降级:默认给一套 flex 或单列流式布局,再用 `@supports` 给支持 Grid 的浏览器升级。这样老浏览器看到的是能用的单列,不是散架的三列。 ``` /* 兜底:单列流式 */ .grid { display: block; } .grid > * { margin-bottom: 16px; } /* 支持 Grid 的浏览器走网格 */ @supports (display: grid) { .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 16px; } .grid > * { margin-bottom: 0; } } ``` 还有两个只在旧版 Safari 上遇到的细节: - Safari 15.4 之前,`gap` 在 `inline-grid` 上不生效。容器老老实实写 `display: grid`,别用缩写偷懒。 - 要兼容更早的版本,用老属性名 `grid-column-gap` / `grid-row-gap` 并排写一份,新浏览器会以后面的 `gap` 为准。 ## **一份能直接拿去用的代码** 把上面几条合起来,就是一个我常用的卡片网格模板。复制进 HTML 文件就能跑,缩放窗口可以看到列数自己变。 ```
卡片标题