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

一看到这种不规则拼贴的效果,我第一反应就是用 CSS Grid 。Grid 在处理二维布局的时候是真的好用,不光能精确控制行和列,还能用 grid-template-areas
直接把布局语义化。尤其是遇到响应式场景时,Grid 简直就是开发效率和界面一致性的神器。
实现思路
常规的 Grid 写法就是在样式里给每个子元素设置它占几行几列、行高列宽多少。但问题是:如果要搞一堆不同的模板,那每次都要写一堆样式,维护起来挺麻烦。而且要是整个模块需要做缩放(比如响应式适配),光靠设置行列宽高也不好用,就算用 clamp
也还是怪怪的。
像这种情况,直接用 grid-template-areas
会舒服很多。
比如我们把这个布局拆成最小的"方格单元",最后抽象成一个 6 列 × 4 行 的网格。格子之间留固定间隙(比如 0.89rem
),要求每个小格子保持正方形,并且能随容器宽度自适应。
模板可以这样定义:
ts
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:
tsx
<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 的区域分布了。

不过问题来了:小格子不是正方形,行高和列宽有 1~2px 的偏差。
为什么会有偏差?
原因其实很简单:
- 列宽计算时,会减去所有横向 gap,然后平均分给列
- 行高的
1fr
是"分剩下的空间",没考虑纵向 gap
结果就是算出来有点对不齐,如果展示图片,可能会导致图片模糊。
解决办法:计算容器的正确高度
本质问题就是 Grid 容器的高度没算准。要让每个格子保持正方形比例,我们可以自己用 padding-top
把高度撑出来。
公式大概是这样的:
text
容器高度 = 行数 × 每列宽度 + (行数 - 1) × gap
每列宽度 = (100% - (列数 - 1) × gap) / 列数
所以最后写法是:
tsx
paddingTop: `calc(${rows} * ((100% - ${columns - 1} * ${gap}) / ${columns}) + ${rows - 1} * ${gap})`
实战代码
tsx
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 还有不少响应式写法:
- auto-fit / auto-fill 自动列
- aspect-ratio 直接控制比例
- 媒体查询切列数
- grid-auto-rows + minmax 动态行高
- 固定 px + auto-fill 保证无偏差
具体写法我就不在这啰嗦了,反正都能解决不同场景的问题。
总结
这次主要踩的坑就是 格子不是正方形,最后靠算高度搞定了。
整个过程下来,我觉得几个要点特别有用:
- 模板化 :用
GRID_AREAS_TEMPLATE
来抽象布局,换场景只改配置 - 自适应 :
padding-top
算高度,保证格子比例 - 多方案结合:Grid 其实提供了很多玩法(auto-fit、aspect-ratio、minmax...),要灵活用
所以无论是做图片墙、仪表盘,还是游戏地图,都能找到适合的 Grid 方案。