六边形及其他:响应式网格图案,无需媒体查询

2025-06-10

六边形及其他:响应式网格图案,无需媒体查询

自从 Flexbox 和 CSS Grid 兴起以来,大家都在喊着同一句话:“浮动已死!”,“别再用浮动了!”但我在这里要复活我们的老朋友浮动,来创建使用 Flexbox 和 CSS Grid 无法实现的复杂且响应迅速的布局。所有这些都无需任何媒体查询即可实现。

我知道这有点难以置信。那么我们先来看一个可以运行的 demo:

这是一个完全响应式的六边形网格,无需使用媒体查询、JavaScript 或大量 CSS 代码。调​​整演示屏幕大小,即可见证它的神奇之处。除了响应式设计外,该网格还可以缩放。例如,我们可以通过添加更多 div 来添加更多六边形,并使用 CSS 变量控制其大小和间距。

很酷吧?这只是我们将以相同方式构建的众多网格中的一个例子。

这篇文章是上一篇文章的通用版本,其中我仅讨论了六边形:https://dev.to/afif/responsive-hexagon-grid-without-media-query-57g7


制作六边形网格

首先,我们创建六边形。使用 很容易完成这个任务clip-path。我们将考虑一个变量S来定义元素的尺寸。Bennett Feely 的Clippy是一款很棒的在线剪切路径生成器。

六边形

每个六边形都是一个inline-block元素。标记可以像这样:

<div class="main">
  <div class="container">
    <div></div>
    <div></div>
    <div></div>
    <!--etc. -->
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

…还有 CSS:

.main {
  display: flex; /* we will talk about this later ... */
  --s: 100px;  /* size  */
  --m: 4px;   /* margin */
}

.container {
  font-size: 0; /* disable white space between inline block element */
}

.container div {
  width: var(--s);
  margin: var(--m);
  height: calc(var(--s) * 1.1547);
  display: inline-block;
  font-size: initial; /* we reset the font-size if we want to add some content */
  clip-path: polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%);
}
Enter fullscreen mode Exit fullscreen mode

到目前为止,一切都还不算太复杂。我们有一个主元素,它包含一个容器,容器又包含六边形。由于我们处理的是inline-block,所以我们需要(使用技巧)解决常见的空白问题,font-size并考虑使用一些边距(用变量 定义M)来控制空间。

到目前为止的结果如下:

每隔一行需要一定的负偏移量,以便行之间重叠,而不是直接堆叠在一起。该偏移量等于元素高度的 25%(参见图 1)。我们将该偏移量应用于margin-bottom以下情况:

.container div {
  width: var(--s);
  margin: var(--m);
  height: calc(var(--s) * 1.1547);
  display: inline-block;
  font-size: initial;
  clip-path: polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%);
  margin-bottom: calc(var(--m) - var(--s) * 0.2886); /* some negative margin to create overlap */
}
Enter fullscreen mode Exit fullscreen mode

…结果变成:

现在真正的诀窍在于如何移动第二行,从而得到一个完美的六边形网格。我们已经把网格挤压到垂直方向互相重叠的程度,但我们需要的是将每隔一行向右移动,这样六边形就会交错排列,而不是重叠。这就是floatshape-outside发挥作用的地方。

你有没有想过为什么我们要用一个.main元素包裹容器,并且使用display: flex?这div也是技巧的一部分。在上一篇文章中,我使用了浮动,并且需要那个 flexbox 容器才能使用height: 100%。在这里,我将做同样的事情。

.container::before {
  content: "";
  width: calc(var(--s)/2 + var(--m));
  float: left;
  height: 100%;
}
Enter fullscreen mode Exit fullscreen mode

我使用container::before伪元素创建了一个浮动元素,它占据了网格左侧的所有高度,宽度等于半个六边形(加上它的边距)。结果如下:

现在,我们可以开始使用 了shape-outside。让我们快速回顾一下它的功能:

CSSshape-outside属性定义了一个形状(可以是非矩形),相邻的内联内容应该围绕该形状进行换行。默认情况下,内联内容会围绕其外边距框进行换行;shape-outside 提供了一种自定义换行的方式,使文本能够围绕复杂的对象(而不是简单的框)进行换行。参考

注意定义中的“内联内容”。这恰恰解释了为什么六边形需要作为inline-block元素。但为了理解我们需要什么样的形状,让我们放大这个图案。

很酷的是shape-outside,它居然能和渐变色兼容。但是,哪种渐变色适合我们的情况呢?

例如,如果我们有 10 行六边形,我们只需要每偶数行移动一次均值。换个角度来看,我们需要每隔一行移动一次,所以我们需要一种重复——这对于重复渐变来说非常完美!

我们将创建一个具有两种颜色的渐变:

  • 透明的可以创建“自由空间”,同时允许第一行保持在原位(如上图蓝色箭头所示)。
  • 不透明颜色将第二行向右移动,这样六边形就不会直接堆叠在一起(由绿色箭头表示)。

我们的shape-outside价值将如下所示:

shape-outside: repeating-linear-gradient(#0000 0 A, #000 0 B); /* #0000 = transparent */
Enter fullscreen mode Exit fullscreen mode

A现在,让我们找到和的值BB将简单地等于两行的高度,因为我们的逻辑需要重复每两行。

两行的高度等于两个六边形的高度(包括它们的边距)减去两倍的重叠部分(2*Height + 4*M - 2*Height*25% = 1.5*Height + 4*M)。或者,在 CSS 中这样表示calc()

calc(1.732 * var(--s) + 4 * var(--m))
Enter fullscreen mode Exit fullscreen mode

好多啊!所以,让我们把这些都放到一个 CSS 自定义属性里F

的值A(由上图中的蓝色箭头定义)至少需要等于一个六边形的大小,但也可以更大。为了将第二行向右移动,我们需要几个不透明颜色的像素,因此A可以简单地等于B - Xpx,其中X是一个较小的值。

我们最终得到如下结果:

shape-outside: repeating-linear-gradient(#0000 0 calc(var(--f) - 3px),#000 0 var(--f));
Enter fullscreen mode Exit fullscreen mode

结果如下:

看到了吗?我们重复的线性渐变的形状是每隔一行向右推六边形宽度的一半,以偏移图案。

让我们把这些放在一起:

.main {
  display:flex;
  --s: 100px;  /* size  */
  --m: 4px;    /* margin */
  --f: calc(var(--s) * 1.732 + 4 * var(--m) - 1px); 
}

.container {
  font-size: 0; /* disable white space between inline block element */
}

.container div {
  width: var(--s);
  margin: var(--m);
  height: calc(var(--s) * 1.1547);
  display: inline-block;
  font-size:initial;
  clip-path: polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%);
  margin-bottom: calc(var(--m) - var(--s) * 0.2885);
}

.container::before {
  content: "";
  width: calc(var(--s) / 2 + var(--m));
  float: left;
  height: 120%; 
  shape-outside: repeating-linear-gradient(#0000 0 calc(var(--f) - 3px), #000 0 var(--f));
}
Enter fullscreen mode Exit fullscreen mode

就这样!只需不到 15 个 CSS 声明,我们就拥有了一个响应式网格,可以完美适配所有屏幕尺寸,而且我们只需控制两个变量就能轻松调整。

你可能注意到了,我给-1px变量添加了一个值F。由于我们处理的计算涉及小数,舍入可能会产生不好的结果。为了避免这种情况,我们添加或移除了一些像素。出于类似的原因,我也使用 而120%不是 来100%表示浮动元素的高度。这些值没有特定的逻辑;我们只是调整它们以确保覆盖大多数情况,而不会导致形状错位。


想要更多形状吗?

用这种方法,我们能做的不仅仅是六边形!我们来创建一个“菱形”网格。同样,我们从clip-path创建形状开始:

菱形剪辑路径形状

代码基本相同,变化的是计算方式和数值。下表说明了这些变化。

六边形网格 菱形网格
height calc(var(--s)*1.1547) var(--s)
clip-path polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%) polygon(50% 0, 100% 50%, 50% 100%, 0 50%)
margin-bottom calc(var(--m) - var(--s)*0.2885) calc(var(--m) - var(--s)*0.5)
--f calc(var(--s)*1.7324 + 4*var(--m)) calc(var(--s) + 4*var(--m))

大功告成!只需对代码进行四处修改,就能得到一个形状不同的全新网格。


这到底有多灵活?

我们了解了如何使用完全相同的代码结构但不同的计算来制作六边形和菱形网格。

让我再给你一个惊喜:把那个计算过程变成一个变量怎么样?这样我们就可以在不修改代码的情况下轻松地在不同的网格之间切换。我们当然可以做到!

我们将使用八边形,因为它更像是一种通用形状,我们只需更改几个值就可以使用它来创建其他形状(六边形、菱形、矩形等)。

八边形剪切路径

我们的八边形由四个变量定义:

  • S:宽度。
  • R:该比例将帮助我们根据宽度定义高度。
  • hcvc:这两个将控制我们的clip-path价值观和我们想要得到的形状。hc将基于宽度,而vc高度

我知道它看起来很重,但它clip-path是用八个点定义的(如图所示)。添加一些 CSS 变量,我们得到如下效果:

clip-path: polygon(
   var(--hc) 0, calc(100% - var(--hc)) 0, /* 2 points at the top */
   100% var(--vc),100% calc(100% - var(--vc)), /* 2 points at the right */
   calc(100% - var(--hc)) 100%, var(--hc) 100%, /* 2 points at the bottom */
   0 calc(100% - var(--vc)),0 var(--vc) /* 2 points at the left */
);
Enter fullscreen mode Exit fullscreen mode

这就是我们的目标:

让我们放大来识别不同的值:

每行之间的重叠(由红色箭头表示)可以使用 vc 变量来表示,该变量给我们一个margin-bottom等于M - vc(其中M是我们的边距)。

除了我们在元素之间应用的边距之外,我们还需要额外的水平边距(黄色箭头所示),其值为S - 2*hc。让我们为水平边距定义另一个变量(MH),其值为M + (S - 2*hc)/2

两行的高度等于形状大小的两倍(加上边距)减去重叠部分的两倍,即2*(S + 2*M) - 2*vc

让我们更新我们的值表来查看如何计算不同网格之间的事物:

六边形网格 菱形网格 八角网格
height calc(var(--s)*1.1547) var(--s) calc(var(--s)*var(--r)))
clip-path polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%) polygon(50% 0, 100% 50%, 50% 100%, 0 50%) polygon(var(--hc) 0, calc(100% - var(--hc)) 0,100% var(--vc),100% calc(100% - var(--vc)), calc(100% - var(--hc)) 100%,var(--hc) 100%,0 calc(100% - var(--vc)),0 var(--vc))
--mh calc(var(--m) + (var(--s) - 2*var(--hc))/2)
margin var(--m) var(--m) var(--m) var(--mh)
margin-bottom calc(var(--m) - var(--s)*0.2885) calc(var(--m) - var(--s)*0.5) calc(var(--m) - var(--vc))
--f calc(var(--s)*1.7324 + 4*var(--m)) calc(var(--s) + 4*var(--m)) calc(2*var(--s) + 4*var(--m) - 2*var(--vc))

好的,让我们根据这些调整来更新我们的 CSS:

.main {
  display: flex;
  --s: 100px;  /* size  */
  --r: 1; /* ratio */

  /* clip-path parameter */
  --hc: 20px; 
  --vc: 30px;

  --m: 4px; /* vertical margin */
  --mh: calc(var(--m) + (var(--s) - 2*var(--hc))/2); /* horizontal margin */
  --f: calc(2*var(--s) + 4*var(--m) - 2*var(--vc) - 2px);
}

.container {
  font-size: 0; /* disable white space between inline block element */
}

.container div {
  width: var(--s);
  margin: var(--m) var(--mh);
  height: calc(var(--s)*var(--r));
  display: inline-block;
  font-size: initial;
  clip-path: polygon( ... );
  margin-bottom: calc(var(--m) - var(--vc));
}

.container::before {
  content: "";
  width: calc(var(--s)/2 + var(--mh));
  float: left;
  height: 120%; 
  shape-outside: repeating-linear-gradient(#0000 0 calc(var(--f) - 3px),#000 0 var(--f));
}
Enter fullscreen mode Exit fullscreen mode

可以看到,代码结构是一样的。我们只是添加了更多变量来控制形状并扩展margin属性。

下面是一个实际的例子。调整不同的变量来控制形状,同时拥有完全响应式的网格:

你说这是互动演示?没错!

为了使事情变得简单,我将vc和表示hc为宽度和高度的百分比,这样我们就可以轻松地缩放元素而不会破坏clip-path

通过以上我们可以很容易的得到初始的六边形网格:

菱形网格:

还有另一个六边形网格:

类似砖石的网格:

我们正在做的是棋盘:

创建任意形状的响应式网格,可能性非常多!我们只需要调整几个变量即可。


修复对齐

让我们尝试控制形状的对齐方式。由于我们处理的是inline-block元素,所以我们会使用默认的左对齐方式,并在末尾留出一些空白,具体留空取决于视口宽度。

请注意,我们根据屏幕宽度在两种网格之间交替:

  • 网格 #1:每行有不同数量的项目(N、N-1、N、N-1 等)
  • 网格 #2:每行项目数相同(N、N、N、N 等)

最好始终有一个网格(#1 或#2)并将所有内容置于中心,以便两侧的可用空间均等划分。

为了获得上图中的第一个网格,容器宽度需要是一个形状的大小的乘数,加上它的边距,或者N*(S + 2*MH),其中N是一个整数值。

这听起来用 CSS 实现似乎不可能,但确实是可以的。我使用 CSS 网格实现了它:

.main {
  display: grid;
  grid-template-columns: repeat(auto-fit, calc(var(--s) + 2*var(--mh)));
  justify-content: center;
}

.container {
  grid-column: 1/-1;
}
Enter fullscreen mode Exit fullscreen mode

.main现在是一个网格容器。使用grid-template-columns,我定义列宽(如前所述),并使用该auto-fit值将尽可能多的列放入可用空间。然后,.container使用 — 跨越所有网格列,1/-1 这意味着我们容器的宽度将是一列大小的倍数。

使事物居中只需要justify-content: center

是的,CSS 很神奇!

调整演示的大小并注意我们不仅拥有图中的第一个网格,而且所有内容也都完美居中。

但是等等,我们移除display: flex并替换了display: grid……那么浮动的百分比高度怎么还能正常工作呢?我说过使用弹性容器才是关键,不是吗?

好吧,事实证明 CSS 网格也支持这个功能。规范如下:

一旦确定了每个网格区域的尺寸,网格项就会被布局到各自的包含块中。为此,网格区域的宽度和高度被认为是确定的。
注意:由于仅使用确定尺寸计算的公式(例如拉伸适配公式)也是确定的,因此拉伸后的网格项的尺寸也被认为是确定的。

网格项stretch默认具有对齐方式,因此其高度是确定的,这意味着使用百分比作为其中的高度是完全有效的。

假设我们想要图中的第二个网格 - 我们只需添加一个额外的列,其宽度等于其他列宽度的一半:

.main {
  display: grid;
  grid-template-columns: repeat(auto-fit,calc(var(--s) + 2*var(--mh))) calc(var(--s)/2 + var(--mh));
  justify-content :center;
}
Enter fullscreen mode Exit fullscreen mode

现在,除了具有足够灵活性以采用自定义形状的完全响应式网格之外,一切都完美居中!


等等,还有一个:金字塔网格

让我们运用所学知识,再构建一个令人惊叹的网格。这次,我们将把刚刚制作的网格改造成金字塔形。

需要注意的是,与我们目前为止制作的网格不同,元素的数量非常重要,尤其是在响应式部分。我们需要知道元素的数量,更精确地说,是行数。

这并不意味着我们需要一堆硬编码值;而是我们使用一个额外的变量来根据行数进行调整。

该逻辑基于行数,因为不同数量的元素可能会产生相同的行数。例如,当元素数量在 11 到 15 个之间时,即使最后一行未完全占用,也会有 5 行。当元素数量在 16 到 21 个之间时,则会有 6 行,以此类推。行数就是我们的新变量。

在深入研究几何和数学之前,这里有一个工作演示:

请注意,大部分代码与前面示例中的代码相同。因此,我们来重点看一下我们添加的新属性:

.main {
  --nr: 5;  /* number of rows */
}

.container {
  max-width: calc(var(--nr)*(var(--s) + 2*var(--mh)));
  margin: 0 auto;
}

.container::before ,
.container i {
  content: "";
  width: calc(50% - var(--mh) - var(--s)/2);
  float: left;
  height: calc(var(--f)*(var(--nr) - 1)/2);
  shape-outside: linear-gradient(to bottom right, #000 50%, #0000 0);
}

.container i {
  float:right;
  shape-outside: linear-gradient(to bottom left, #000 50%, #0000 0);
}
Enter fullscreen mode Exit fullscreen mode

NR是我们的行数变量。容器的宽度需要等于金字塔的最后一行,以确保它能容纳所有元素。查看上图,您会发现最后一行包含的项目数恰好等于行数,这意味着公式为:NR* (S + 2*MH)

你可能也注意到了,我们还在<i>其中添加了一个元素。这样做是因为我们需要两个浮动元素,我们将在其中应用shape-outside

为了理解为什么我们需要两个浮动元素,让我们看看幕后做了什么:

蓝色元素是我们的浮动元素。每个元素的宽度等于容器大小的一半减去形状大小的一半,再加上边距。在我们的例子中,高度等于四行,NR - 1在更一般的情况下,高度等于。之前我们定义了两行的高度,F即 ,所以一行的高度是F/2。就这样,我们得到了height: calc(var(--f)*(var(--nr) - 1)/2

现在我们有了元素的大小,我们需要对形状的外部应用渐变。

上图中的紫色区域是元素的限制区域(需要不透明)。剩余区域是元素可以流动的自由空间(需要透明)。这可以使用对角渐变来实现:

shape-outside: linear-gradient(to bottom right, #000 50%, #0000 0); 
Enter fullscreen mode Exit fullscreen mode

我们只需将另一个浮动元素的“右”替换为“左”。你可能已经注意到,这并没有响应。事实上,继续调整演示的视口宽度,看看它有多么不响应。

我们有几种方法可以实现响应:

  1. 当容器宽度小于视口宽度时,我们可以回退到第一个网格。虽然代码编写起来有点棘手,但它可以让我们保持元素的大小一致。
  2. 我们可以减小元素的尺寸,以保持金字塔网格的完整性。使用基于百分比的值技巧更容易实现,但这可能会导致元素在较小的屏幕上显示得非常小。

我们先来个第一个方案吧。我们喜欢挑战,对吧?

为了获得金字塔网格,我们需要两个浮动元素。初始网格只需要一个浮动元素。幸运的是,得益于伪元素,我们的结构允许我们拥有三个浮动元素,而无需在标记中添加其他元素。我们将使用container::beforei::beforei::after

/* Same as before... */

/* The initial grid */
.container::before {
  content: "";
  width: calc(var(--s)/2 + var(--mh));
  float: left;
  height: 120%; 
  shape-outside: repeating-linear-gradient(#0000 0 calc(var(--f) - 3px),#000 0 var(--f));
}

/* The pyramidal grid */
.container i::before ,
.container i::after {
  content: "";
  width: calc(50% - var(--mh) - var(--s)/2);
  float: left;
  height: calc(var(--f)*(var(--nr) - 1)/2);
  shape-outside: linear-gradient(to bottom right,#000 50%,#0000 0);
}

.container i::after {
  float:right;
  shape-outside: linear-gradient(to bottom left,#000 50%,#0000 0);
}
Enter fullscreen mode Exit fullscreen mode

现在我们需要一个技巧,让我们可以只使用第一个浮动元素,或者其他两个浮动元素,但不能同时使用它们。这个条件应该基于我们容器的宽度:

  • 如果容器宽度大于最后一行的宽度,我们可以使用金字塔并使用其中的浮动元素<i>
  • 如果容器宽度小于最后一行的宽度,我们就切换到另一个网格并使用第一个浮动元素。

我们可以用clamp()它来实现它!它有点像一个条件函数,设置一个最小值和最大值的范围,并在这个范围内,我们提供一个在这两个点之间使用的“理想”值。这样,我们就可以使用公式作为限定值在网格之间“切换”,同时仍然避免使用媒体查询。

我们的代码看起来是这样的:

.main {
  /* the other variables won't change*/
  --lw: calc(var(--nr)*(var(--s) + 2*var(--mh))); /* width of last row */
}

.container {
  max-width: var(--lw);
}

/* The initial grid */
.container::before {
  width: clamp(0px, (var(--lw) - 100%)*1000, calc(var(--s)/2 + var(--mh)));
}

/* The pyramidal grid */
.container i::before,
.container i::after {
  width: clamp(0px, (100% - var(--lw) + 1px)*1000, calc(50% - var(--mh) - var(--s)/2));
}
Enter fullscreen mode Exit fullscreen mode

在更大的屏幕上,容器的宽度(LW)现在等于它的max-width,所以100% == LW。这意味着 的宽度.container::before等于0px(并导致此浮动元素被禁用)。

对于其他浮动元素,我们限制其宽度:

width: clamp(0px, (100% - var(--lw) + 1px)*1000, calc(50% - var(--mh) - var(--s)/2));
Enter fullscreen mode Exit fullscreen mode

…中间值((100% - LW + 1px)*1000)等于(0 + 1px)*1000 = 1000px(一个故意设得很大的任意值)。它被限制在calc(50% - var(--mh) - var(--s)/2)。换句话说,这些浮动元素启用了正确的宽度(我们之前定义的宽度)。

瞧!我们在大屏幕上看到了一个金字塔形状。

现在,当容器宽度变小时,LW会大于100%。因此(LW - 100%)是正数。乘以一个大值后,它会被限制在calc(var(--s)/2 + var(--mh)),从而启用第一个浮动元素。对于其他浮动元素,(100% - LW + 1px)解析为负值并被限制在0px,从而禁用浮动元素。

调整以下演示的大小,看看我们如何在两个网格之间切换

让我们尝试添加更多元素:

看到了吗?一切都完美缩放了。我们还可以将其与之前使用的 CSS 网格对齐技巧结合起来:

您认为现在“浮动”是一件很糟糕的事情吗?

想要倒置金字塔吗?

如上图所示,对之前的代码进行两处修改可以颠倒我们的金字塔:

  • 我将渐变的方向从to bottom left|right改为to top left|right
  • 我添加了一个margin-top等于一行高度的值。

而且,嘿,我们可以轻松地在两个金字塔之间切换:

是不是很漂亮?我们有一个自定义形状的响应式金字塔网格,可以轻松反转,并在小屏幕上回退到另一个响应式网格,同时所有内容都完美居中。所有这些都没有使用任何媒体查询或 JavaScript,而是使用了经常被忽视的浮动属性。

在某些特定情况下,您可能会注意到一些对齐问题。是的,这又是一个与我们的计算以及我们试图在交互式演示中使其通用相关的舍入问题。为了解决这个问题,我们只需手动调整几个值(尤其是渐变的百分比),直到恢复完美对齐。


这是一个 漂浮裹!

就是这样:结合float可以shape-outside帮助我们制作复杂、灵活和响应迅速的布局——浮动万岁!

文章到此结束,但这仅仅是个开始。我提供了布局,现在你可以轻松地在 div 中放置任何内容,并应用背景、阴影、动画等。

链接:https://dev.to/this-is-learning/hexagons-and-beyond-responsive-grid-patterns-sans-media-queries-1nb4
PREV
如何使用 Git 命令清理本地存储库
NEXT
您应该了解的 7 个开源项目 - Python 版 ✔️ pandas:强大的 Python 数据分析工具包 Apache Airflow Ultroi​​d - UserBot 部署文档教程 Zulip 概述