六边形及更多:无需 Media Queries 的灵活响应式 Grid 布局
[中文正文内容]
Temani Afif 于 2021 年 6 月 8 日
之前,Chris 分享了一个漂亮的六边形 Grid。顾名思义,它使用 CSS Grid 来形成该布局。这是一个巧妙的技巧!结合 grid columns、grid gaps 和创造性的 clipping,最终呈现出效果。
类似的效果也可以用 flexbox 实现。但我在这里要复活我们的老朋友 float
,来创建相同类型的复杂且响应式的布局,但复杂度更低,而且不需要任何 media query。
我知道这很难相信。让我们从一个可用的演示开始:
CodePen Embed Fallback
这是一个完全响应式的六边形 grid,没有使用 media query、JavaScript 或大量的 hacky CSS。调整演示屏幕大小,看看奇迹。除了响应式之外,grid 还可以缩放。例如,我们可以通过添加更多的 divs 来放入更多的六边形,并使用 CSS 变量来控制尺寸和间距。
很酷,对吧?这只是我们以相同方式构建的众多 grid 中的一个例子。
创建六边形 Grid
首先,我们创建六边形形状。使用 clip-path
可以很容易地完成这项任务。我们将考虑一个变量 S
,它将定义元素的大小。Bennett Feely 的 Clippy 是一个很棒的在线 clip-path
生成器。
使用
clip-path
创建六边形形状
每个六边形都是一个 inline-block
元素。标记可以是这样的:
<div class="main">
<div class="container">
<div></div>
<div></div>
<div></div>
<!--等等-->
</div>
</div>
...以及 CSS:
.main {
display: flex; /* 我们稍后会讨论这个... */
--s: 100px; /* size */
--m: 4px; /* margin */
}
.container {
font-size: 0; /* 禁用 inline block 元素之间的空格 */
}
.container div {
width: var(--s);
margin: var(--m);
height: calc(var(--s) * 1.1547);
display: inline-block;
font-size: initial; /* 如果我们要添加一些内容,我们会重置 font-size */
clip-path: polygon(0% 25%, 0% 75%, 50% 100%, 100% 75%, 100% 25%, 50% 0%);
}
到目前为止没什么复杂的。我们有一个 main 元素,它包含一个 container,而 container 又包含六边形。由于我们正在处理 inline-block
,我们需要解决常见的空格问题(使用 font-size
技巧),并且我们考虑一些 margin(用变量 M
定义)来控制空间。
切换第一个演示的 font-size 以说明空格问题
这是目前的结果:
每隔一行都需要一些负偏移,以便行重叠而不是直接堆叠在彼此之上。该偏移量将等于元素高度的 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); /* 一些负 margin 以创建重叠 */
}
...结果变成:
现在的真正诀窍是我们如何移动第二行以获得一个完美的六边形 grid。我们已经将事物压缩到行在垂直方向上相互重叠的程度,但我们需要的是将每隔一行向右推,以便六边形交错而不是重叠。这就是 float
和 shape-outside
发挥作用的地方。
您是否想知道为什么我们有一个 .main
元素包裹着我们的 container 并具有 display: flex
?该 div 也是该技巧的一部分。在之前的文章中,我使用了 float
,并且我需要那个 flexbox container 才能使用 height: 100%
。我在这里也会做同样的事情。
.container::before {
content: "";
width: calc(var(--s)/2 + var(--m));
float: left;
height: 100%;
}
我正在使用 container::before
伪元素来创建一个 float 元素,该元素占据 grid 左侧的所有高度,并且其宽度等于半个六边形(加上其 margin)。我们得到以下结果:
黄色区域是我们的
.container::before
伪元素。
现在,我们可以使用 shape-outside
。让我们快速回顾一下它的作用。Robin 在 CSS-Tricks Almanac 中很好地定义了它。MDN 也对其进行了很好的描述:
shape-outside
CSS 属性定义了一个形状(可以是任意形状),相邻的 inline content 应该围绕该形状进行环绕。默认情况下,inline content 围绕其 margin box 环绕;shape-outside
提供了一种自定义此环绕的方式,从而可以使文本围绕复杂对象而不是简单的框进行环绕。 重点是我加的
请注意定义中的“inline content”。这正好解释了为什么六边形需要是 inline-block
元素。但是要了解我们需要什么样的形状,让我们放大该模式。
shape-outside
的酷之处在于它实际上可以与 gradients 一起使用。但是什么样的 gradient 适合我们的情况呢?
例如,如果我们有 10 行六边形,我们只需要移动每 偶数 行。换句话说,我们需要移动每隔一行,因此我们需要一种重复形式,非常适合 repeating gradient!
我们将创建一个具有两种颜色的 gradient:
- 一种透明色,用于创建“自由空间”,同时允许第一行保持原位(如上图中的蓝色箭头所示)。
- 一种不透明色,用于将第二行向右移动,以便六边形不直接堆叠在彼此之上(如绿色箭头所示)。
我们的 shape-outside
值将如下所示:
shape-outside: repeating-linear-gradient(#0000 0 A, #000 0 B); /* #0000 = transparent */
现在,让我们找到 A
和 B
的值。B
将简单地等于两行的高度,因为我们的逻辑需要每两行重复一次。
两行的高度等于两个六边形的高度(包括它们的 margins),减去两次重叠(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
应用于 float 元素,创建了一个具有预定线性 gradient 的浮动区域。
看到了吗?我们的 repeating linear gradient 的形状正在将每隔一行向右推半个六边形的宽度,以偏移该模式。
让我们将所有这些放在一起:
.main {
display:flex;
--s: 100px; /* size */
--m: 4px; /* margin */
--f: calc(var(--s) * 1.732 + 4 * var(--m) - 1px);
}
.container {
font-size: 0; /* 禁用 inline block 元素之间的空格 */
}
.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 声明,我们就可以获得一个响应式 grid,该 grid 可以很好地适应所有屏幕尺寸,并且我们可以通过简单地控制两个变量来轻松调整事物。
您可能已经注意到我在变量 F
中添加了 -1px
。由于我们正在处理涉及小数的计算,因此舍入可能会给我们带来不良结果。为避免这种情况,我们添加或删除几像素。我还为 float 元素的高度使用了 120%
而不是 100%
,原因类似。这些值没有特别的逻辑;我们只是调整它们以确保涵盖大多数情况,而不会使我们的形状错位。
想要更多形状?
我们可以使用这种方法做更多的事情,而不仅仅是六边形!让我们创建一个“菱形” grid。同样,我们从 clip-path
开始创建形状:
使用 clip-path 的菱形形状
代码基本上是相同的。正在更改的是计算和值。在下面找到一个表格,该表格将说明这些更改。
六边形 grid | 菱形 grid
---|---
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))
我们完成了!仅对我们的代码进行了四个更改,就可以得到一个全新的 grid,但形状不同。
CodePen Embed Fallback
这种方法有多灵活?
我们看到了如何使用完全相同的代码结构但不同的变量来制作六边形和菱形 grid。让我用另一个想法让您大吃一惊:如果我们将该计算变为变量,以便我们可以轻松地在不同的 grid 之间切换而无需更改代码怎么办?我们当然可以这样做!
我们将使用八边形形状,因为它是更通用的形状,我们可以仅通过更改几个值来使用它来创建其他形状(六边形,菱形,矩形等)。
此八边形形状上的点在
clip-path
属性中定义。
我们的八边形由四个变量定义:
S
:宽度。R
:该比率将帮助我们根据宽度定义高度。hc
和vc
:这两个都将控制我们的clip-path
值和我们想要获得的形状。hc
将基于宽度,而vc
将基于高度
我知道它看起来很繁重,但是 clip-path
是使用八个点定义的(如图所示)。添加一些 CSS 变量,我们得到以下信息:
clip-path: polygon(
var(--hc) 0, calc(100% - var(--hc)) 0, /* 顶部的 2 个点 */
100% var(--vc),100% calc(100% - var(--vc)), /* 右侧的 2 个点 */
calc(100% - var(--hc)) 100%, var(--hc) 100%, /* 底部的 2 个点 */
0 calc(100% - var(--vc)),0 var(--vc) /* 左侧的 2 个点 */
);
这是我们的目标:
让我们放大以识别不同的值:
每一行之间的重叠(用红色箭头表示)可以使用 vc
变量表示,该变量为我们提供了一个等于 M - vc
的 margin-bottom
(其中 M
是我们的 margin)。
除了我们在元素之间应用的 margin 之外,我们还需要一个额外的水平 margin(用黄色箭头表示),等于 S - 2*hc
。让我们为水平 margin(MH
)定义另一个变量,该变量等于 M + (S - 2*hc)/2
。
两行的高度等于一个形状的两倍大小(加上 margin),减去两次重叠,或 2*(S + 2*M) - 2*vc
。
让我们更新我们的值表,以查看我们如何在不同 grid 之间计算事物:
六边形 grid | 菱形 grid | 八边形 grid
---|---|---
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; /* 禁用 inline block 元素之间的空格 */
}
.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
属性。
下面是一个工作示例。调整不同的变量以控制形状,同时拥有一个完全响应的 grid:
CodePen Embed Fallback
您说这是一个互动演示吗?你打赌!
CodePen Embed Fallback
为了简化操作,我将 vc
和 hc
表示为宽度和高度的百分比,因此我们可以轻松地缩放元素而不会破坏 clip-path
从上面,我们可以轻松获得初始的六边形 grid:
菱形 grid:
另一个六边形 grid:
类似 Masonry 的 grid:
还有一个跳棋盘:
很多可能性来创建具有任何类型形状的响应式 grid!我们所要做的就是调整几个变量。
修复对齐方式
让我们尝试控制形状的对齐方式。由于我们正在处理 inline-block
元素,因此我们正在处理默认的左对齐以及末尾的一些空白空间,具体取决于 viewport 宽度。
请注意,我们根据屏幕宽度在两种类型的 grid 之间交替:
Grid #1:每行不同数量的 items(
N
,N-1
,N
,N-1
等)Grid #2:每行相同数量的 items(N
,N
,N
,N
等)
最好始终具有其中一个 grid(无论是 #1 还是 #2),并居中所有内容,以便空白空间在两侧平均分配。
为了获得上图中第一个 grid,container 宽度需要为一个形状大小(加上其 margin)的倍数,或 N*(S + 2*MH)
,其中 N
是一个整数值。
这对于 CSS 来说似乎是不可能的,但实际上是可能的。我使用 CSS grid 完成了它:
.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 container。使用 grid-template-columns
,我定义了列宽(如前所述),并使用 auto-fit
值以在可用空间中尽可能多地获取列。然后,.container
使用 1/-1
跨越所有 grid 列,这意味着我们的 container 的宽度将是一列大小的倍数。
居中所有内容只需要 justify-content: center
。
是的,CSS 是魔术!
CodePen Embed Fallback
调整演示大小,并注意我们不仅具有图中第一个 grid,而且一切也都完美居中。
但是等等,我们删除了 display: flex
并换成了 display: grid
……那么基于百分比的 float 高度如何仍然有效?我说过使用 flex container 是关键,不是吗?
好吧,事实证明 CSS grid 也具有该功能。从规范中:
一旦确定了每个 grid area 的大小,grid items 将被布置到它们各自的包含块中。grid area 的宽度和高度被认为是为此目的的 definite。 注意:由于仅使用 definite 大小计算的公式(例如 stretch fit 公式)也是 definite 的,因此拉伸的 grid item 的大小也被认为是 definite 的。
默认情况下,grid item 具有 stretch
对齐方式,因此其高度是 definite 的,这意味着在其内部使用百分比作为高度是完全有效的。
假设我们改为想要图中的第二个 grid,我们只需添加一个额外的列,其宽度等于其他列宽度的一半:
.main {
display: grid;
grid-template-columns: repeat(auto-fit,calc(var(--s) + 2*var(--mh))) calc(var(--s)/2 + var(--mh));
justify-content :center;
}
CodePen Embed Fallback
现在,除了一个完全响应式 grid,该 grid 足够灵活以采用自定义形状外,一切也都完美居中!
解决溢出问题
在最后一个 items 上使用负 margin-bottom
并且 float 元素推动我们的 items 会产生一些不需要的溢出,这可能会影响放置在我们 grid 之后的内容。
CodePen Embed Fallback
如果调整演示大小,您会注意到一个等于负偏移的溢出,有时会更大。解决方法是向我们的 container 添加一些 padding-bottom
。我将使 padding 等于一个形状的高度:
CodePen Embed Fallback
我必须承认,没有一个完美的解决方案来解决溢出问题并控制我们 grid 下方的空间。该空间取决于许多因素,我们可能必须为每种情况使用不同的 padding 值。最安全的解决方案是考虑一个涵盖大多数情况的大值。
再等一下:金字塔形 grid
让我们采用我们所学的一切并构建另一个惊人的 grid。这次,我们将我们刚刚制作的 grid 转换为金字塔形的 grid。
考虑到与我们到目前为止制作的 grid 不同,元素的数量很重要,尤其是在响应部分中。需要知道元素的数量,更确切地说,是行数。
基于 items 数量的不同金字塔形 grid
这并不意味着我们需要一堆硬编码的值;而是我们使用一个额外的变量来根据行数调整事物。
该逻辑基于行数,因为不同数量的元素可能会给我们提供相同数量的行。例如,当我们有 11 到 15 个元素时,有五行,即使最后一行未完全占用。拥有 16 到 21 个元素会给我们六行,依此类推。行数是我们的新变量。
在深入研究几何形状和数学之前,这是一个工作演示:
CodePen Embed Fallback
请注意,大多数代码与我们在先前的示例中所做的相同。因此,让我们专注于我们添加的新属性:
.main {
--nr: 5; /* 行数 */
}
.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
是我们的行数变量。container 的宽度需要等于金字塔的最后一行,以确保它容纳所有元素。如果您检查先前的图,您会看到最后一行中包含的 items 数量简单地等于行数,这意味着公式为:NR* (S + 2*MH)
。
您可能还注意到我们还在那里添加了一个 <i>
元素。我们这样做是因为我们需要两个浮动元素,我们将在其中应用 shape-outside
。
为了理解为什么我们需要两个浮动元素,让我们看看幕后发生的事情:
金字塔形 grid
蓝色元素是我们的浮动元素。每个元素的宽度都等于 container 大小的一半,减去形状大小的一半加上 margin。在本例中,高度等于四行,更一般的情况下等于 NR - 1
。早些时候,我们定义了两行的高度 F
,因此一行的