六边形及其他:灵活、响应式的网格模式,无需媒体查询

Avatar of Temani Afif
Temani Afif

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 $200 免费积分!

不久前,Chris 分享了 这个不错的六边形网格。顾名思义,它使用的是——等等——CSS Grid 来形成这种布局。这是一个很酷的技巧!结合网格列、网格间隙和创造性的裁剪,最终产生了结果。

使用 flexbox 也可以实现类似的效果。但我在这里是要复活我们老朋友 float,以创建相同类型的复杂且响应式的布局——但复杂度更低,而且没有使用任何媒体查询。

我知道,很难相信。所以,让我们从一个有效的演示开始。

这是一个完全响应式的六边形网格,无需媒体查询、JavaScript 或大量 hacky CSS。调整演示屏幕大小,观察魔法效果。除了响应式,网格还可以缩放。例如,我们可以通过添加更多 div 来添加更多六边形,并使用 CSS 变量来控制大小和间距。

很酷,对吧?这只是我们用相同方式构建的许多网格中的一个例子。

创建六边形网格

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

使用 clip-path 创建六边形

每个六边形都是一个 inline-block 元素。标记可以类似于以下代码:

<div class="main">
  <div class="container">
    <div></div>
    <div></div>
    <div></div>
    <!--etc. -->
  </div>
</div>

…以及 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%);
}

到目前为止,没有复杂的地方。我们有一个主元素,它包含一个容器,容器又包含六边形。由于我们处理的是 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 */
}

…结果如下:

现在真正的技巧是如何移动第二行以获得完美的六边形网格。我们已经将事物压缩到行在垂直方向上重叠的地步,但我们需要做的是将每隔一行向右推,以便六边形交错而不是重叠。这就是 floatshape-outside 发挥作用的地方。

您是否想知道为什么我们的 .main 元素包装我们的容器,并具有 display: flex?该 div 也是技巧的一部分。在 之前的一篇文章 中,我使用了 float,我需要这个 flexbox 容器才能使用 height: 100%。我在这里将做同样的事情。

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

我使用 container::before 伪元素来创建一个浮动元素,该元素占用网格左侧的所有高度,并且宽度等于六边形的一半(加上其边距)。我们得到以下结果:

黄色区域是我们的 .container::before 伪元素。

现在,我们可以使用 shape-outside。让我们快速回顾一下它的作用。Robin 在 CSS-Tricks 年鉴中 很好地定义了它。MDN 也很好地描述了它:

shape-outside CSS 属性定义了一个形状——可能是非矩形的——相邻的 内联内容 应该围绕它进行包裹。默认情况下,内联内容围绕其边距框进行包裹;shape-outside 提供了一种自定义这种包裹的方法,使得围绕复杂对象而不是简单盒子进行文本包裹成为可能。

重点是我加的。

注意定义中的“内联内容”。这解释了为什么六边形需要是 inline-block 元素。但是,要了解我们需要什么样的形状,让我们放大一下模式。

shape-outside 的妙处在于它实际上可以与渐变一起使用。但什么样的渐变适合我们的情况呢?

例如,如果我们有 10 行六边形,我们只需要移动 偶数 行的平均值。换句话说,我们需要移动每隔一行,所以我们需要一种重复——非常适合重复渐变!

我们将创建一个包含两种颜色的渐变:

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

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

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

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

两行的高度等于两个六边形的高度(包括其边距),减去两次重叠(2*Height + 4*M - 2*Height*25% = 1.5*Height + 4*M)。或者,用 CSS 中的 calc() 表示:

calc(1.732 * var(--s) + 4 * var(--m))

这太多了!所以,让我们把这一切都放在一个 CSS 自定义属性 F 中。

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

最终结果如下:

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

以及以下结果:

shape-outside 应用于浮动元素,创建一个具有预设线性渐变的浮动区域。

看到了吗?我们重复的线性渐变形状将每隔一行向右推半六边形的宽度,以抵消模式。

让我们把这一切都放在一起:

.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));
}

就是这样!只需不到 15 个 CSS 声明,我们就获得了响应式网格,它可以很好地适应所有屏幕尺寸,而且我们可以轻松地通过简单地控制两个变量来调整内容。

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

想要更多形状?

我们不仅可以使用这种方法创建六边形!让我们改用“菱形”网格。同样,我们从 clip-path 开始创建形状:

使用 clip-path 创建菱形

代码基本相同。变化的是计算和值。下表将说明这些变化。

六边形网格菱形网格
高度calc(var(--s)*1.1547)var(--s)
clip-pathpolygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%)polygon(50% 0, 100% 50%, 50% 100%, 0 50%)
margin-bottomcalc(var(--m) - var(--s)*0.2885)calc(var(--m) - var(--s)*0.5)
--fcalc(var(--s)*1.7324 + 4*var(--m))calc(var(--s) + 4*var(--m))

就这样!只需对代码进行四个更改,我们就可以得到一个全新的网格,但形状不同。

这到底有多灵活?

我们看到了如何使用完全相同的代码结构,但不同的变量来创建六边形和菱形网格。让我用另一个想法来震撼你的心智:将该计算设为一个变量,以便我们可以轻松地在不同网格之间切换,而无需更改代码?我们当然可以做到!

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

八边形形状上的点是在 clip-path 属性中定义的。

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

  • 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 */
);

这就是我们的目标:

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

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

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

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

让我们更新我们的值表,看看我们如何在不同的网格之间进行计算:

六边形网格菱形网格八边形网格
高度calc(var(--s)*1.1547)var(--s)calc(var(--s)*var(--r)))
clip-pathpolygon(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))
--mhcalc(var(--m) + (var(--s) - 2*var(--hc))/2)
marginvar(--m)var(--m)var(--m) var(--mh)
margin-bottomcalc(var(--m) - var(--s)*0.2885)calc(var(--m) - var(--s)*0.5)calc(var(--m) - var(--vc))
--fcalc(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));
}

正如我们所见,代码结构是一样的。我们只是添加了更多变量来控制形状并扩展 `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;
}

现在 `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;
}

现在,除了一个完全响应式且足够灵活以接受自定义形状的网格之外,所有内容都完美地居中!

解决溢出问题

在最后一个项目上使用负 `margin-bottom` 和浮动元素推动我们的项目将产生一些不希望的溢出,这可能会影响放在网格之后的內容。

如果调整演示的大小,你会注意到一个等于负偏移量的溢出,有时还会更大。解决方法是在容器中添加一些 `padding-bottom`。我将使填充等于一个形状的高度。

我不得不承认,没有完美的解决方案可以解决溢出问题并控制网格下方空间。该空间取决于很多因素,我们可能需要针对每种情况使用不同的填充值。最安全的解决方案是考虑一个覆盖大多数情况的大值。

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

让我们把学到的所有东西都拿来,构建另一个令人惊叹的网格。这次,我们将把我们刚刚构建的网格转换为金字塔形的。

考虑到与我们迄今为止构建的网格不同,元素的数量很重要,尤其是在响应式部分。需要知道元素的数量,更确切地说,是行的数量。

基于项目数量的不同金字塔形网格

这并不意味着我们需要一大堆硬编码的值;相反,我们使用一个额外的变量来根据行的数量调整事物。

逻辑基于行的数量,因为不同数量的元素可能会给我们相同数量的行。例如,当我们有 11 到 15 个元素时,即使最后一行没有完全填满,也有五行。有 16 到 21 个元素会给我们六行,依此类推。行的数量是我们的新变量。

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

请注意,大多数代码与我们在前面示例中所做的相同。所以让我们关注我们添加的新属性。

.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);
}

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

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

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

A pyramid grid of octagon shapes. The octagons alternate between green and red. There are 5 rows of octagons.
金字塔形网格

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

现在我们已经有了元素的大小,我们需要在我们的 `shape-outside` 中应用一个渐变。

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

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

我们只需将另一个浮动元素的 `right` 更改为 `left` 即可。你可能已经注意到,这不是响应式的。实际上,继续调整演示的视口宽度,看看它有多么不响应。

我们有几个选项可以实现响应。

  1. 当容器宽度小于视口宽度时,我们可以回退到第一个网格。这在编码方面有点棘手,但它允许我们保留元素的相同大小。
  2. 我们可以减小元素的大小以保持金字塔形网格。这在使用基于百分比的值技巧方面更容易编码,但这会导致在较小的屏幕尺寸上出现非常小的元素。

让我们选择第一个解决方案。我们喜欢挑战,对吧?

为了获得金字塔形网格,我们需要两个浮动元素。初始网格只需要一个浮动元素。幸运的是,我们的结构允许我们拥有**三个**浮动元素,而无需在标记中添加更多元素,这要归功于伪元素。我们将使用 `container::before`、`i::before`、`i::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);
}

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

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

我们可以为此使用 `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));
}

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

对于其他浮动元素,我们夹紧宽度

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

…其中间值 ((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 属性。

你可能会注意到在某些特定情况下存在一些错位。是的,这又是与我们正在进行的计算以及我们试图通过交互式演示使其通用化有关的舍入问题。为了纠正这一点,我们只需手动调整一些值(尤其是渐变的百分比),直到我们获得完美的对齐。

这是一个浮动包裹!

我们得到了:将 floatshape-outside 相结合可以帮助我们制作复杂的、灵活的和响应式布局——float 长存不衰!

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