普通视图

发现新文章,点击刷新页面。
昨天以前首页

关于 Grid 布局的一种响应式应用

作者 叮叮叮398
2025年9月3日 13:48

前言

最近在做一个需求,要在页面上做一个图片墙,要求是展示不同大小的图片,但又要排成一个固定的模板样子。

大概像这样:

企业微信截图_1756794007605.png

一看到这种不规则拼贴的效果,我第一反应就是用 CSS Grid。Grid 在处理二维布局的时候是真的好用,不光能精确控制行和列,还能用 grid-template-areas 直接把布局语义化。尤其是遇到响应式场景时,Grid 简直就是开发效率和界面一致性的神器。


实现思路

常规的 Grid 写法就是在样式里给每个子元素设置它占几行几列、行高列宽多少。但问题是:如果要搞一堆不同的模板,那每次都要写一堆样式,维护起来挺麻烦。而且要是整个模块需要做缩放(比如响应式适配),光靠设置行列宽高也不好用,就算用 clamp 也还是怪怪的。

像这种情况,直接用 grid-template-areas 会舒服很多。

比如我们把这个布局拆成最小的“方格单元”,最后抽象成一个 6 列 × 4 行 的网格。格子之间留固定间隙(比如 0.89rem),要求每个小格子保持正方形,并且能随容器宽度自适应。

模板可以这样定义:

const GRID_AREAS_TEMPLATE = {
  ONE: [
    ["A","A","A","B","C","C"],
    ["A","A","A","D","C","C"],
    ["A","A","A","E","F","F"],
    ["G","H","I","J","F","F"],
  ],
};

不同的字母就代表不同的区域,子元素通过 gridArea 对应。这样一来,我们可以提前准备多个模板,按场景动态切换。


React 示例

比如这里我直接写了一个 Demo:

<div
  style={{
    gridTemplateColumns: "repeat(6, 1fr)",
    gridTemplateRows: "repeat(4, 1fr)",
    gridTemplateAreas: template.map(row => `"${row.join(" ")}"`).join(" "),
    gap: "0.89rem",
  }}
>
  {imgsEls.map((el, index) => {
    const area = String.fromCharCode(65 + index); // A,B,C...
    return (
      <div
        key={index}
        style={{ gridArea: area }}
        className="flex items-center justify-center"
      >
        {el}
      </div>
    );
  })}
</div>

打开控制台就能看到 Grid 的区域分布了。

企业微信截图_17567938641448.png

不过问题来了:小格子不是正方形,行高和列宽有 1~2px 的偏差。


为什么会有偏差?

原因其实很简单:

  • 列宽计算时,会减去所有横向 gap,然后平均分给列
  • 行高的 1fr 是“分剩下的空间”,没考虑纵向 gap

结果就是算出来有点对不齐,如果展示图片,可能会导致图片模糊。


解决办法:计算容器的正确高度

本质问题就是 Grid 容器的高度没算准。要让每个格子保持正方形比例,我们可以自己用 padding-top 把高度撑出来。

公式大概是这样的:

容器高度 = 行数 × 每列宽度 + (行数 - 1) × gap
每列宽度 = (100% - (列数 - 1) × gap) / 列数

所以最后写法是:

paddingTop: `calc(${rows} * ((100% - ${columns - 1} * ${gap}) / ${columns}) + ${rows - 1} * ${gap})`

实战代码

const columns = 6;
const rows = 4;
const gap = "0.89rem";
const gridTemplateAreas = GRID_AREAS_TEMPLATE.ONE.map(row => `"${row.join(" ")}"`).join(" ");

return (
  <div
    className="relative w-full"
    style={{
      paddingTop: `calc(${rows} * ((100% - ${columns - 1} * ${gap}) / ${columns}) + ${rows - 1} * ${gap})`,
    }}
  >
    <div
      className="absolute inset-0 grid h-full w-full"
      style={{
        gridTemplateColumns: `repeat(${columns}, 1fr)`,
        gridTemplateRows: `repeat(${rows}, 1fr)`,
        gridTemplateAreas,
        gap,
      }}
    >
      {imgsEls.map((el, index) => {
        const area = String.fromCharCode(65 + index); // A, B, C...
        return (
          <div
            key={index}
            style={{ gridArea: area }}
            className="flex items-center justify-center"
          >
            {el}
          </div>
        );
      })}
    </div>
  </div>
);

这样就能保证:

  • 容器高度和宽度匹配,格子永远是正方形
  • 布局用 grid-template-areas 来控制,清晰直观
  • 想换模板的时候只改配置就行

其他可选方案

除了上面这种 padding-top 算高度的办法,其实 Grid 还有不少响应式写法:

  1. auto-fit / auto-fill 自动列
  2. aspect-ratio 直接控制比例
  3. 媒体查询切列数
  4. grid-auto-rows + minmax 动态行高
  5. 固定 px + auto-fill 保证无偏差

具体写法我就不在这啰嗦了,反正都能解决不同场景的问题。


总结

这次主要踩的坑就是 格子不是正方形,最后靠算高度搞定了。

整个过程下来,我觉得几个要点特别有用:

  1. 模板化:用 GRID_AREAS_TEMPLATE 来抽象布局,换场景只改配置
  2. 自适应padding-top 算高度,保证格子比例
  3. 多方案结合:Grid 其实提供了很多玩法(auto-fit、aspect-ratio、minmax…),要灵活用

所以无论是做图片墙、仪表盘,还是游戏地图,都能找到适合的 Grid 方案。

❌
❌