响应式无缝网格布局:auto-fit + minmax() 之外,真正会崩的几个地方

发布于
2

上个月接手一个电商老项目,首页商品墙在 iPhone 上一列、iPad 上两列、27 寸显示器上——居然还是两列,右边空出老大一块白。同事挺委屈:”CSS 明明写的是响应式啊。”我打开一看,grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)) 一字不差。

这行代码确实没错,错的是它周围那一圈东西:视口标签、盒模型、图片尺寸、hover 动画,任何一处没管住,网格就会以各种姿势崩给你看。下面按我实际排查的顺序写,都是真踩过的坑。

viewport 没写,后面全白干

排查那个商品墙时我第一件事是看 <head>,果然没写 viewport。这是最容易被忽略的一条,因为它在电脑上看不出任何问题——你在 Chrome 里拖窗口,网格照样老老实实地变列数,一切正常,于是你以为代码是对的。

但手机浏览器不按你想的来。缺了这个标签,它会拿一个约 980px 的虚拟视口去渲染整页,再整体缩小塞进屏幕。结果就是:@media 的断点压根不触发,auto-fit 算出来的列数也是基于 980px 算的。你在真机上看到的”两列”,其实是 980px 宽下算出的两列被缩到了屏幕大小。

<!-- 必须硬编码在 head 里,别用 JS 动态插入 -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">

就这一行。它决定了后面所有响应式逻辑有没有机会执行。

别加 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 文件就能跑,缩放窗口可以看到列数自己变。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <style>
    *, *::before, *::after { box-sizing: border-box; }

    .grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr));
      gap: clamp(12px, 2vw, 24px);
      padding: 0; margin: 0; list-style: none;
    }

    .card {
      min-width: 0;
      min-height: 180px;
      background: #fff;
      border: 1px solid transparent;   /* 先占位,hover 只改颜色 */
      border-radius: 10px;
      overflow: hidden;
      transition: transform .18s ease, box-shadow .18s ease;
    }
    .card:hover {
      transform: translateY(-2px);
      box-shadow: 0 8px 20px rgba(0,0,0,.08);
      border-color: #e5e8eb;
    }

    .card img {
      display: block; width: 100%; height: auto;
      aspect-ratio: 4 / 3; object-fit: cover;
    }
    .card p {
      margin: 12px; overflow-wrap: anywhere;
    }
  </style>
</head>
<body>
  <ul class="grid">
    <li class="card"><img src="cover.jpg" alt=""><p>卡片标题</p></li>
    <!-- 更多卡片 -->
  </ul>
</body>
</html>

收工前自查这五条

  1. <head> 里有 viewport,且没写 user-scalable=no

  2. minmax() 的上限是 1fr,不是任何像素值

  3. 间距只用 gap,子项身上没有任何 margin

  4. 全局 box-sizing: border-box,图片有 width: 100% 和 height: auto

  5. 卡片有 min-height,hover 只改 transform 和 opacity

说到底,响应式网格的”无缝”不是那一行 CSS 自动达成的,而是你把四周会漏的地方都堵上之后的结果。下次再遇到列数不对,别急着改 minmax() 里的数字,按上面这个顺序从头过一遍,通常第二关就找到了。

0 / 600
0 讨论
热门最新
总结
暂无总结
嗨,下午好!
所有的成功,都源自一个勇敢的开始

暂无福利券