最近我遇到一个独特的布局需求:在页面包含全屏元素的同时,保持一个元素始终固定在顶部。这实现起来相当棘手,因此我将记录下我的解决方案,以备不时之需。尤其是在小屏幕上的逻辑定位处理上,更是增加了难度。
效果难以用文字描述,所以我录制了屏幕视频来说明我的意思。请特别注意主要的号召性用语部分,也就是带有“立即体验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解决方案。让我们把脏活留给浏览器吧!
你对此有更好的解决方案或不同的方法吗?我很乐意听到你的想法。
अस्वीकरण: उपलब्ध कराए गए सभी संसाधन आंशिक रूप से इंटरनेट से हैं। यदि आपके कॉपीराइट या अन्य अधिकारों और हितों का कोई उल्लंघन होता है, तो कृपया विस्तृत कारण बताएं और कॉपीराइट या अधिकारों और हितों का प्रमाण प्रदान करें और फिर इसे ईमेल पर भेजें: [email protected] हम इसे आपके लिए यथाशीघ्र संभालेंगे।
Copyright© 2022 湘ICP备2022001581号-3