原文链接 Creating horizontal scrolling containers the right way [CSS Grid] -- 作者 Dannie Vinther
自从奈飞 Netflix
成为了家喻户晓的名字以来,在移动端中我们一直使用横向布局。水平滚动容器(列表)已经成为了一种常见的布局做法,而不是将东西都堆叠在页面上,这将减少占用小屏幕设备垂直的空间。
本文,我们探讨 CSS
网格的弹性布局,它是如何帮助我们实现水平滚动的,同时处理它带来的缺陷。
UX(用户体验)的考虑
UX/UE -> User Experience 译者加
本文不会深入讨论水平滚动的用户体验方面。但是,当采用水平滚动布局时,至少需要满足两点 UX
原则:
- 你的设计必须在视觉上提醒他人,这是一组可以水平滚动的内容。最好的方法,就是让可滚动的内容露出一部分。
- 用户知道什么时候滚到末尾,这很重要。我们注意到用户重复进行滚动操作,是因为他们认为自己并未充分滚动。一种方法指明列表已经滚到最后:在列表末尾使用额外的空间
布局大纲
开始前,我们概览下需要实现的布局特性:
- 滚动的容器必须准守页面的整体布局。比如,外边距和内边距整体要一致。
- 滚动的部分内容,必须在容器边缘露出来。
- 滚动时,容器的内容必须从屏幕的边缘滑出来。
- 容器内两个内容之间的距离要小于边缘的距离,这样容器两端都会有更大的空间(这提示用户他们已经滑到最后)。
如下:
需要注意的是,容器两端的距离和周围内容的距离是匹配的(也就是整体布局要和谐)。
整体布局
现在,我们已经基本明白水平滚动容器的特性了。接下来,我们考虑使用 CSS Grid
网格布局来编码。使用 CSS Grid
网格布局方便我们控制元素之间的距离,无需进一步计算。
对于整体布局,我们将使用简单但强大的 CSS Grid
技术:
.app {
display: grid;
grid-template-columns: 20px 1fr 20px;
}
.app > * {
grid-column: 2 / -2;
}
.app > .full {
grid-column: 1 / -1;
}
.app
类元素下的子元素都会被“容器化”,它们都有 20px
的边距,使得内容远离边缘。带 .full
类名的子元素,将会占据全部视窗的宽度且没有内边距。
滚动容器
我们使用六个卡片来创建水平滚动容器,一次显示两张。因为我们考虑整体布局,水平滚动的两边填充内边距,我们删除了 .full 类,然后添加如下:
.hs {
display: grid;
grid-gap: 10px;
grid-template-columns: repeat(6, calc(50% - 40px));
grid-template-rows: minmax(150px, 1fr);
}
使用 grid-template-columns,我们可以设置每个卡片需要的空间。在这个例子中,卡片占有视图空间的 50%
减去间隔 40px
。这时候,我们会看到第三张卡片露出来。
然而,需要注意的是,卡片两端被砍断部分。还记得不,当水平滚动的时候,我们希望可滚动的内容是从屏幕的边缘滑出。
所以,我们在容器中添加 .full
类,并填补缺失的内边距。
.hs {
display: grid;
grid-gap: 10px;
grid-template-columns: repeat(6, calc(50% - 40px));
grid-template-rows: minmax(150px, 1fr);
padding: 0 20px; // 添加
}
乍一看,我们好像实现了需求,但是当你滚动到尾部的时候,你会注意到并没有其他空间了 -- 所以这并不符合整体布局。
你可能想在最后一个元素添加 margin-right
的属性值以处理这个问题:
.hs > li:last-child {
margin-right: 20px;
}
很不幸,这并不起作用。那么,我们要怎么处理呢?
建议的解决方案
考虑我们目前都有了些什么内容,我们删除容器中的内边距:
.hs {
display: grid;
grid-gap: 10px;
grid-template-columns: repeat(6, calc(50% - 40px));
grid-template-rows: minmax(150px, 1fr);
}
如果我们在 grid-template-columns 两边添加内边距,会实现我们要的布局。
我们在网格列两端添加了 2 x 10px
的空间。结合 10px
的网格距离,我们总共有 20px
,所以满足我们整体布局的内边距要求。
.hs {
display: grid;
grid-gap: 10px;
grid-template-columns: // 更改
10px
repeat(6, calc(50% - 40px))
10px;
grid-template-rows: minmax(150px, 1fr);
}
为了不让第一张卡片占用第一列的 10px
的空间,我们在每一端引入空的伪元素:
.hs::before,
.hs::after {
content: ‘’;
}
伪元素 ::before
和 ::after
非常适合 grid-columns
布局,因为会自动添加到水平滚动容器的开头和结尾。伪元素能够参与网格化布局让人心存感激。
现在,我们实现了一开始在大纲中提到的特性。
注意事项
这项技术的一个注意事项是在 grid-template-columns 中对既定卡片数量的计算。
grid-template-columns:
10px
repeat(6, calc(50% - 40px))
10px;
如果容器中只是包含 4
个卡片,你需要为该特定容器设定新的网格规则。这不是很灵活。
一种使其更灵活的处理方式是,你可以使用 Javascript
来计算卡片的数量,然后将其分配给 CSS
变量。
var root = document.documentElement;
const lists = document.querySelectorAll('.hs');lists.forEach(el => {
const listItems = el.querySelectorAll('li');
const n = el.children.length;
el.style.setProperty('--total', n);
});
然后,你就可以在 grid-template-columns 中使用变量:
grid-template-columns:
10px
repeat(var(--total) , calc(50% - 40px)) // 重点
10px;
更新: 如 Alex Baciu 提及,我们可以通过使用隐式网格完全省略 Javascript
(或者 CSS
变量解决方案)。这样,我们不需要计算超出列的数量,因为这是浏览器为我们计算的。
为此,我们调整下代码:
.hs {
...
grid-template-columns: 10px;
grid-auto-flow: column;
grid-auto-columns: calc(50% - var(--gutter) * 2);
...
....hs:before,
.hs:after {
content: '';
width: 10px;
}
我们仍然需要最初的 10px
内边距来弥补不足,然而,剩下的卡片通过自动放置算法布局。为此,我们需要设置 grid-auto-flow
为 column
(默认值是 row
)。
最后,我们需要确保的是 .hs:after
,它继承了其他卡片的大小,其占用的空间不超过 10px
。所以我们需要通过固定的宽度来限定。
你可能会争辩,代码变得不那么清晰了,因为赋值更加分散,使得正在发生的东西变得混乱。但是,我觉得还行 :)
译者加:本文滚动的技术交流为主,熟悉其原理。真正业务上操作,建议使用成熟的 Swiper 操作。