最近我遇到一个独特的布局需求:在页面包含全屏元素的同时,保持一个元素始终固定在顶部。这实现起来相当棘手,因此我将记录下我的解决方案,以备不时之需。尤其是在小屏幕上的逻辑定位处理上,更是增加了难度。
效果难以用文字描述,所以我录制了屏幕视频来说明我的意思。请特别注意主要的号召性用语部分,也就是带有“立即体验Domino”标题的部分。
我们的目标是在较大视窗中,将主要的号召性用语显示在右侧,而用户向下滚动时,其他部分会从其下方经过。在较小视窗中,号召性用语元素必须显示在带有“开始试用”标题的主要英雄区之后。
这里主要有两个挑战:
在深入探讨几种可能的解决方案(及其局限性)之前,让我们先搭建语义化的HTML结构。
构建此类布局时,可能会倾向于创建重复的号召性用语部分:一个用于桌面版本,另一个用于移动版本,然后在适当的时候切换它们的可见性。这避免了在HTML中寻找完美位置以及应用处理两种布局需求的CSS的麻烦。我必须承认,我有时也会这样做。但这一次,我想避免重复我的HTML代码。
另一个需要考虑的是,我们对.box--sticky
元素使用了粘性定位,这意味着它需要与其他元素(包括全屏元素)同级,才能正常工作。
以下是标记:
英雄区粘性区全屏区全屏区
在CSS网格布局中创建粘性元素非常简单。我们向.box--sticky
元素添加position: sticky
和top: 0
偏移量,指示其开始粘贴的位置。注意,我们只在视窗宽度大于768px时才使元素粘性化。
@media screen and (min-width: 768px) { .box--sticky { position: sticky; top: 0; } }
需要注意的是,当position: sticky
与overflow: auto
一起使用时,Safari中存在已知的粘性定位问题。Caniuse网站的已知问题部分对此进行了说明:
设置为
overflow: auto
的父元素会阻止Safari中position: sticky
的工作。
不错,这很容易。接下来让我们解决全屏元素的挑战。
第一个方案是我经常使用的方法:绝对定位的伪元素,可以从一侧延伸到另一侧。这里的技巧是使用负偏移量。
如果我们讨论的是居中内容,那么计算就相当简单:
.box--bleed { max-width: 600px; margin-right: auto; margin-left: auto; padding: 20px; position: relative; } .box--bleed::before { content: ""; background-color: dodgerblue; position: absolute; top: 0; bottom: 0; right: calc((100vw - 100%) / -2); left: calc((100vw - 100%) / -2); }
简而言之,负偏移量是视窗宽度(100vw
)减去元素宽度(100%
),然后除以-2,因为我们需要两个负偏移量。
需要注意的是,使用100vw
时存在一个已知的bug,Caniuse网站也对此进行了说明:
目前除Firefox之外的所有浏览器都错误地认为
100vw
是整个页面宽度,包括垂直滚动条,当设置overflow: auto
时,这可能会导致水平滚动条。
现在让我们在内容不居中的情况下创建全屏元素。如果你再次观看视频,你会注意到粘性元素下方没有内容。我们不希望粘性元素与内容重叠,这就是为什么在这个特定的布局中没有居中内容的原因。
首先,我们将创建网格:
.grid { display: grid; grid-gap: var(--gap); grid-template-columns: var(--cols); max-width: var(--max-width); margin-left: auto; margin-right: auto; }
我们使用自定义属性,允许我们重新定义最大宽度、间隙和网格列,而无需重新声明属性。换句话说,我们重新声明变量值,而不是重新声明grid-gap
、grid-template-columns
和max-width
属性:
:root { --gap: 20px; --cols: 1fr; --max-width: calc(100% - 2 * var(--gap)); } @media screen and (min-width: 768px) { :root { --max-width: 600px; --aside-width: 200px; --cols: 1fr var(--aside-width); } } @media screen and (min-width: 980px) { :root { --max-width: 900px; --aside-width: 300px; } }
在宽度为768px及以上的视窗上,我们定义了两列:一列具有固定宽度(--aside-width
),另一列填充剩余空间(1fr
),以及网格容器的最大宽度(--max-width
)。
在小于768px的视窗上,我们定义了一列和间隙。网格容器的最大宽度是视窗的100%,减去两侧的间隙。
现在是精彩的部分。内容在较大的视窗上没有居中,因此计算不像你想象的那么简单。以下是它的样子:
.box--bleed { position: relative; z-index: 0; } .box--bleed::before { content: ""; display: block; position: absolute; top: 0; bottom: 0; left: calc((100vw - (100% var(--gap) var(--aside-width))) / -2); right: calc(((100vw - (100% - var(--gap) var(--aside-width))) / -2) - (var(--aside-width))); z-index: -1; }
我们没有使用父元素宽度的100%,而是考虑了间隙和粘性元素的宽度。这意味着全屏元素中的内容宽度不会超过英雄元素的边界。这样,我们确保粘性元素不会与任何重要信息重叠。
左侧偏移量比较简单,因为我们只需要从视窗宽度(100vw
)中减去元素宽度(100%
)、间隙(--gap
)和粘性元素(--aside-width
)。
left: (100vw - (100% var(--gap) var(--aside-width))) / -2);
右侧偏移量比较复杂,因为我们必须将粘性元素的宽度添加到之前的计算中,--aside-width
,以及间隙,--gap
:
right: ((100vw - (100% var(--gap) var(--aside-width))) / -2) - (var(--aside-width) var(--gap));
现在我们可以确保粘性元素不会与全屏元素中的任何内容重叠。
这是一个包含水平bug的解决方案:
这是一个包含水平bug修复的解决方案:
修复方法是隐藏body的x轴溢出,这通常也是一个好主意:
body { max-width: 100%; overflow-x: hidden; }
这是一个完全可行的解决方案,我们可以在此结束。但这有什么乐趣呢?通常不止一种方法可以完成某件事,所以让我们看看另一种方法。
我们可以通过配置网格来实现相同的效果,而不是使用居中的网格容器和伪元素。让我们从定义网格开始,就像我们上次做的那样:
.grid { display: grid; grid-gap: var(--gap); grid-template-columns: var(--cols); }
同样,我们使用自定义属性来定义间隙和模板列:
:root { --gap: 20px; --gutter: 1px; --cols: var(--gutter) 1fr var(--gutter); }
我们在小于768px的视窗上显示三列。中间列占据尽可能多的空间,而另外两列仅用于强制水平间隙。
@media screen and (max-width: 767px) { .box { grid-column: 2 / -2; } }
请注意,所有网格元素都放置在中间列中。
在大于768px的视窗上,我们定义了一个--max-width
变量,它限制了内部列的宽度。我们还定义了--aside-width
,即粘性元素的宽度。同样,这样可以确保粘性元素不会定位在全屏元素内的任何内容之上。
:root { --gap: 20px; } @media screen and (min-width: 768px) { :root { --max-width: 600px; --aside-width: 200px; --gutter: calc((100% - (var(--max-width))) / 2 - var(--gap)); --cols: var(--gutter) 1fr var(--aside-width) var(--gutter); } } @media screen and (min-width: 980px) { :root { --max-width: 900px; --aside-width: 300px; } }
接下来,我们将计算边距宽度。计算公式是:
--gutter: calc((100% - (var(--max-width))) / 2 - var(--gap));
…其中100%
是视窗宽度。首先,我们从视窗宽度中减去内部列的最大宽度。然后,我们将结果除以2以创建边距。最后,我们减去网格间隙以获得边距列的正确宽度。
现在让我们将.box--hero
元素推到一边,以便它从网格的第一列开始:
@media screen and (min-width: 768px) { .box--hero { grid-column-start: 2; } }
这会自动推动粘性框,使其紧跟在英雄元素之后。我们也可以明确地定义粘性框的位置,如下所示:
.box--sticky { grid-column: 3 / span 1; }
最后,让我们通过将grid-column
设置为1 / -1
来创建全屏元素。这告诉元素从第一个网格项开始内容,并跨越到最后一个网格项。
@media screen and (min-width: 768px) { .box--bleed { grid-column: 1 / -1; } }
为了居中内容,我们将计算左侧和右侧填充。左侧填充等于边距列的大小加上网格间隙。右侧填充等于左侧填充的大小,再加上另一个网格间隙以及粘性元素的宽度。
@media screen and (min-width: 768px) { .box--bleed { padding-left: calc(var(--gutter) var(--gap)); padding-right: calc(var(--gutter) var(--gap) var(--gap) var(--aside-width)); } }
这是最终的解决方案:
我更喜欢这个解决方案,因为它没有使用有问题的视窗单位。
我喜欢CSS计算。使用数学运算并不总是直截了当的,尤其是在组合不同的单位时,例如100%
。弄清楚100%
的含义是工作的一半。
我也喜欢使用CSS解决简单但复杂的布局,就像这个一样。现代CSS具有原生解决方案——例如网格、粘性定位和计算——消除了复杂且相当繁重的JavaScript解决方案。让我们把脏活留给浏览器吧!
你对此有更好的解决方案或不同的方法吗?我很乐意听到你的想法。
Isenção de responsabilidade: Todos os recursos fornecidos são parcialmente provenientes da Internet. Se houver qualquer violação de seus direitos autorais ou outros direitos e interesses, explique os motivos detalhados e forneça prova de direitos autorais ou direitos e interesses e envie-a para o e-mail: [email protected]. Nós cuidaremos disso para você o mais rápido possível.
Copyright© 2022 湘ICP备2022001581号-3