手机App里那种蜂巢网格布局,到底是怎么用纯CSS做出来的?

2,316字
10–15 分钟
in

许多应用的设计里都能看到六边形网格的身影,那种错落有致的排列看起来确实比普通方块要灵动。但其实背后并没有想象中那么复杂,也不需要用JavaScript去计算每个元素的位置,只用CSS的新特性就能搞定。这个思路的核心是让每个六边形自己“知道”该在什么时候往左挪一点,从而实现行与行之间的自然错位。

目录

现代CSS里的六边形变身术

以前要画个六边形,只能靠clip-path在盒子上切出六个点的多边形,还得手动算宽高比例。现在有了corner-shape属性配合border-radius,画六边形就像调圆角一样简单。直接设置宽度,用aspect-ratio: cos(30deg)自动算出高度比例,再通过border-radiuscorner-shape把边角处理成斜面,一个完整的六边形就出来了。这个方法的好处是,给六边形加边框或者背景都不会有裁切痕迹,而clip-path裁出来的形状加边框会有点麻烦。

搭建会自我调整的弹性容器

容器用display: flex搭配flex-wrap: wrap,加上gap属性来控制间距,这样元素就能自动换行。每个六边形设置固定的宽度,用aspect-ratio: cos(30deg)保持形状比例,border-radius: 50% / 25%corner-shape: bevel把形状变成六边形。为了让下一行的六边形能“嵌”进上一行的缝隙里,每个元素都加上向下的负边距,数值需要根据宽度计算。这个负边距让行与行之间产生重叠,为后面的错位打下基础。

精准定位需要错位的元素

现在需要解决的问题是,每一行里第一个元素需要向左移动半个宽度加半个间距的距离,但只能作用在偶数行的第一个上。由于屏幕宽度变化,每一行能放几个六边形是动态的,所以得用CSS算出来当前元素是不是某一行第一个。先通过容器查询获取容器宽度,除以每个六边形占用的宽度(自身宽度加间距),向下取整得到当前行能放的个数N。对于偶数行,还得考虑第一个元素已经向左移动了,所以每行能放的个数M会少半个间距和半个宽度,同样向下取整。

每个六边形都有一个index序号,现在要判断这个序号是不是落在需要移动的规则里。观察发现,需要移动的元素序号遵循N*i + M*(i-1) + 1这个规律,i从1开始。把这个公式变形,就能用序号表达出i,判断i是不是整数。如果(序号 - 1 + M) / (N + M)的结果是整数,那这个元素就应该有左边距。用sibling-index()函数可以获取当前元素在父级中的序号,配合mod函数取小数部分来判断是不是整数。如果小数部分为0,就说明是整数,需要移动。

通过布尔值控制边距开关

先用round(down, (100cqw + 间距) / (宽度 + 间距))算出N,再用round(down, (100cqw - (宽度 - 间距)/2) / (宽度 + 间距))算出M。接着用sibling-index()获取序号,套进公式(序号 - 1 + M) / (N + M)得到--_i。用1 - mod(--_i, 1)把整数的情况变成1,小数的情况变成0到1之间,再用round四舍五入,就能得到一个布尔值--_c。最后通过calc(var(--_c) * (宽度 + 间距)/2)来决定是否应用左边距。当--_c为1时,左边距生效,为0时就是0。整个过程不需要手动干预,屏幕大小变化时所有值都会重新计算。

.container {
  --s: 120px;
  --g: 10px;
  container-type: inline-size;
  display: flex;
  gap: var(--g);
  flex-wrap: wrap;
}
.item {
  width: var(--s);
  aspect-ratio: cos(30deg);
  border-radius: 50% / 25%;
  corner-shape: bevel;
  margin-bottom: calc(var(--s)/(-4*cos(30deg)));
}
.item {
  --_n: round(down, (100cqw + var(--g))/(var(--s) + var(--g)));
  --_m: round(down, (100cqw - (var(--s) - var(--g))/2)/(var(--s) + var(--g)));
  --_i: calc((sibling-index() - 1 + var(--_m))/(var(--_n) + var(--_m)));
  --_c: round(down, 1 - mod(var(--_i), 1));
  margin-left: calc(var(--_c) * (var(--s) + var(--g))/2);
}

换个形状也能无缝切换

这套逻辑真正灵活的地方在于,需要移动的那部分计算完全不用动,只需要改改形状相关的几个属性。比如换成菱形,宽高比改成1,圆角改成50%corner-shape保留bevel,下边距改成calc(var(--s)/-2),其他代码纹丝不动。换成八边形稍微麻烦点,因为八边形之间的水平间距需要更大,得在容器的--g变量里加上一部分宽度的值。但核心的判断逻辑还是照搬过来。想换成圆形网格,把corner-shape去掉,圆角改成50%,下边距用calc(var(--s)/-2),容器间距保持常规,同样能得到错落排列的圆形布局。

把偏移方向改到奇数行

如果想把偏移效果从偶数行换到奇数行,关键在于调整--_i的计算公式。之前用的是(序号 - 1 + M) / (N + M),现在需要改成(序号 + M) / (N + M),这样原本的整数偏移点就会整体后移一位。再用同样的方法取小数部分转换成布尔值,就能让奇数行第一个元素产生左边距。这个调整说明只要搞清楚序号的数学规律,就能按需控制哪些行产生位移。