你的CSS网格还能这么玩?,试试这个不需要媒体查询的金字塔布局

3,521字
15–22 分钟
in

想用纯CSS搭出一个金字塔形状的网格,还不想写一堆乱七八糟的媒体查询?这事儿放在几年前可能得靠JavaScript硬算,但现在嘛,CSS自己就能搞定。最近捣鼓网格布局的时候发现,用上CSS Grid的自动放置特性,再加上几个现代CSS的数学函数,真能整出个自适应的金字塔结构。整个过程不需要盯着屏幕宽度写断点,也不用JS去动态改样式,纯纯的CSS逻辑就能让这些六边形块自己排成金字塔形,屏幕变窄了还能自动切换成普通网格,挺神奇的吧。

目录

摘要
这篇文章会一步步拆解如何用CSS Grid构建一个响应式金字塔网格,核心思路是让每个元素占据两列,通过动态计算每个元素的grid-column-start值来控制它们在网格中的位置。文章会从基础网格结构搭起,先解决固定数量元素的精准定位问题,然后引入sibling-index()和数学公式来自动处理任意数量的元素,最后通过条件判断实现从金字塔到普通网格的无缝切换。整个过程不依赖任何媒体查询或JavaScript,全靠CSS自身的逻辑能力。

先从网格结构搭起,固定列数这事儿不靠谱

要搭金字塔,得先搞清楚底层的网格架子长啥样。这次不用Flexbox,改用CSS Grid,因为Grid能更精确控制每个元素占几列几行。先看基础HTML结构,就是一堆div装在一个容器里:

<div class="container">
  <div></div>
  <div></div>
  <div></div>
  <!-- 爱放多少放多少 -->
</div>

给容器设置成网格,关键点在于让每列宽度固定,并且用auto-fit让列数自动适应容器宽度。这里有个小设计,为了让六边形块能错开排列,每个块得占两列宽度,所以grid-template-columns里把变量--s重复写了两遍:

.container {
  --s: 40px;  /* 六边形的大小 */
  --g: 5px;   /* 缝隙 */

  display: grid;
  grid-template-columns: repeat(auto-fit, var(--s) var(--s));
  justify-content: center;
  gap: var(--g);
}

这里把--s写两遍不是手误,而是为了保证列数永远是偶数。每个六边形块要跨两列,那网格的列数必须是偶数才能让块排得整整齐齐。再看每个块的样式,沿用了之前文章里六边形的生成方式,但这里的负外边距跟之前不一样了,因为块的宽度变成了2*var(--s) + var(--g),所以计算也要跟着变:

.container > * {
  grid-column-end: span 2;
  aspect-ratio: cos(30deg);
  border-radius: 50% / 25%;
  corner-shape: bevel;
  margin-bottom: calc((2*var(--s) + var(--g))/(-4*cos(30deg)));
}

解决元素落位问题,得给第一列元素排排坐

网格架子搭好了,现在得让这些块按金字塔形状排起来。仔细观察金字塔结构,会发现每行的第一个块(比如第1个、第2个、第4个、第7个、第11个…)的列起始位置是有规律的。这些数字叫三角数序列,公式是j*(j+1)/2 + 1,其中j从0开始。

这些特殊块的位置也不是乱排的,它们跟容器能容纳的块数N有关系。假设网格能容纳N个块(这里的N是按块数算,不是按列数算),那么第1个块应该放在第N列,第2个块放在第N-1列,第4个块放在第N-2列,依此类推。N的值怎么算?得看容器宽度能塞下多少块:

.container {
  container-type: inline-size; /* 把容器变成查询容器,用100cqw获取宽度 */
}

.container > * {
  --_n: round(down,(100cqw + var(--g))/(2*(var(--s) + var(--g))));
}

--_n就是N的值,用round(down,...)向下取整,确保块不会溢出容器。有了这个值,就可以给那串特殊块指定列起始位置了。比如:

.container > *:nth-child(1) { grid-column-start: calc(var(--_n) - 0) }
.container > *:nth-child(2) { grid-column-start: calc(var(--_n) - 1) }
.container > *:nth-child(4) { grid-column-start: calc(var(--_n) - 2) }
/* 后面还有一堆... */

这堆选择器看着就头疼,要是块数多起来,手写肯定不现实。所以得换个思路,用数学公式把这些特殊块一次性筛出来。

用数学公式自动匹配所有特殊块

前面提到三角数序列公式j*(j+1)/2 + 1 = indexindex是元素在DOM里的序号(从1开始)。反过来,给定一个index,怎么判断它是不是属于这个序列?解这个一元二次方程:

j = sqrt(2*index - 1.75) - 0.5

如果算出来的j是整数,那就说明这个元素是每行的第一个块。CSS现在有sibling-index()函数,能直接拿到当前元素在兄弟中的序号,配合sqrt()mod()就能判断了:

.container > * {
  --_n: round(down,(100cqw + var(--g))/(2*(var(--s) + var(--g))));
  --_j: calc(sqrt(2*sibling-index() - 1.75) - .5);
  --_d: mod(var(--_j),1);
  grid-column-start: if(style(--_d: 0): calc(var(--_n) - var(--_j)););
}

--_d--_j的小数部分,如果等于0就说明是整数,那就给这个元素设置列起始位置为var(--_n) - var(--_j)。这一下把所有特殊块都覆盖了,不管加多少元素都能自动匹配。

屏幕变窄怎么办,加个条件让它自己切换

屏幕宽度变小,装不下金字塔完整形状的时候,网格应该自动切换成普通六边形网格。这里需要判断什么时候该切换。当容器宽度小到某个临界点时,金字塔底部的元素会排不下,这时候需要把某些元素左移一列。

具体来说,当金字塔结构被破坏后,每个新行的第一个元素(比如第29个、第42个…)应该从第2列开始。这些元素的位置也有公式可以算出来:

N*i + (N - 1)*(i - 1) + 1 + N*(N - 1)/2 = index

解出i

i = (index - 2 + N*(3 - N)/2)/(2*N - 1)

i是正整数时,说明这个元素是响应式切换后的行首,得把它的列起始位置设为2。这里要注意,原来的金字塔逻辑对某些元素(比如第29个)也会生效,所以得用if()语句把两个条件合起来,并且让响应式条件的优先级更高:

.container > * {
  --_n: round(down,(100cqw + var(--g))/(2*(var(--s) + var(--g))));

  --_j: calc(sqrt(2*sibling-index() - 1.75) - .5);
  --_d: mod(var(--_j),1);

  --_i: calc((sibling-index() - 2 + (var(--_n)*(3 - var(--_n)))/2)/(2*var(--_n) - 1));
  --_c: mod(var(--_i),1);

  grid-column-start:
    if(
      style((--_i > 0) and (--_c: 0)): 2;
      style(--_d: 0): max(0, var(--_n) - var(--_j));
    );
}

这里max(0, ...)是为了防止列起始位置变成负数。负数虽然也是有效值,但会把元素甩到不知道哪去,用max把负值截成0,0在Grid里会被忽略,浏览器就自动用默认值了。

换个花样试试,菱形八角形圆形都能玩

这套思路不光能搭金字塔,换个形状也能玩出新花样。比如把六边形换成菱形,稍微调整一下aspect-ratioborder-radius,效果就完全不一样了。还有八角形、圆形,甚至反过来让金字塔尖朝下,都可以通过调整grid-column-start的计算逻辑来实现。关键在于理解这套“给特定元素设列起始位置,利用Grid自动放置”的核心玩法。

像下面这个菱形网格,缝隙处理方式稍微改了一下,但核心的列定位逻辑跟金字塔是一模一样的:

.rhombus > * {
  aspect-ratio: 1;
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
  /* 其他样式类似 */
}

玩到后面会发现,CSS现在真的能写点带逻辑的布局了,不再是单纯堆属性值。