创建 CSS 轮播

CSS overflow 模块定义了一些特性,可以用来创建灵活且无障碍的纯 CSS 轮播,它带有浏览器生成且可由开发者设置样式的滚动按钮和滚动标记。本指南将解释如何使用这些特性来创建一个轮播。

轮播是 Web 上的一个常见功能。它们通常以滚动内容区域的形式出现,其中包含多个项目,例如演示幻灯片、广告、头条新闻或关键产品特性。

用户可以通过点击或激活导航按钮或通过滑动来浏览这些项目。导航通常包括:

滚动按钮

通常是“上一个”和“下一个”按钮或链接。

滚动标记

一系列按钮或链接图标,每个图标代表一个或多个项目,具体取决于轮播内每个滚动位置显示的项目数量。

A carousel with a content area in the middle, previous and next buttons to the left and right, and scroll markers at the bottom

轮播的一个关键特性是分页——项目感觉像是独立的内容片段,用户在它们之间移动,而不是形成一个连续的内容部分。你可以一次显示一个项目,或者在每个轮播“页面”上显示多个项目。当多个项目可见时,每次按下“下一个”或“上一个”按钮时,你可能会显示一组全新的项目。或者,你可以在列表的一端添加一个新项目,同时将另一端的项目移出视图。

使用 JavaScript 实现轮播可能相当脆弱且具有挑战性。它需要脚本将滚动标记与它们所代表的项目关联起来,同时不断更新滚动按钮以保持其正常运行。当使用 JavaScript 创建轮播时,还必须额外添加轮播和相关控件的无障碍性。

幸运的是,我们可以使用 CSS 轮播特性创建带有相关控件的无障碍轮播,而无需使用 JavaScript。

CSS 轮播特性提供了一些伪元素和伪类,使得仅使用 CSS 和 HTML 就可以创建轮播,浏览器以一种无障碍、灵活且一致的方式处理大部分滚动和链接引用。这些特性如下:

::scroll-button()

这些伪元素在滚动容器内部生成,代表滚动按钮,用于将容器向指定方向滚动。

::scroll-marker-group

在滚动容器内部生成;用于收集和布局滚动标记。

::scroll-marker

在滚动容器祖先元素的子元素内部或滚动容器的列内生成,以表示它们的滚动标记。可以选择这些标记将容器滚动到其关联的子元素或列,它们被收集在滚动容器的 ::scroll-marker-group 中以便进行布局。

:target-current

这个伪类可以用来选择当前激活的滚动标记。它可以用来为当前激活的标记提供高亮样式,这对于可用性和无障碍性非常重要。

::column

当一个容器通过 CSS 多列布局 设置为多列显示其内容时,此伪元素代表生成的各个列。它可以与 ::scroll-marker 结合使用,为每一列生成一个滚动标记。

我们的第一个示例是一个单页轮播,每个项目占据整个页面。我们有滚动标记作为底部导航,以及页面两侧的滚动按钮,使用户能够移动到下一页和上一页。

我们将使用 flexbox 来布局轮播,使用滚动捕捉来强制实现清晰的分页,并使用锚点定位来定位滚动按钮和滚动标记相对于轮播的位置。

HTML 由一个标题元素和一个无序列表组成,每个列表项都包含一些示例内容:

html
<h1>CSS carousel single item per page</h1>
<ul>
  <li>
    <h2>Page 1</h2>
  </li>
  <li>
    <h2>Page 2</h2>
  </li>
  <li>
    <h2>Page 3</h2>
  </li>
  <li>
    <h2>Page 4</h2>
  </li>
</ul>

我们使用 flexbox 来创建一行项目;<ul> 是 flex 容器,其子列表项 <li> 水平显示,每个项目占据轮播的整个宽度。

无序列表通过 100vwwidth 设置来填充视口的全宽;它还被赋予了 300pxheight 和一些 padding。然后我们使用 flexbox 来布局列表——将 display 值设置为 flex,使得子列表项以行显示(由于默认的 flex-direction 值为 row),每个项目之间有 4vwgap

css
ul {
  width: 100vw;
  height: 300px;
  padding: 20px;
  display: flex;
  gap: 4vw;
}

现在是时候为列表项设置样式了。第一个声明提供了基本的样式。重要的声明是 flex 值为 0 0 100%,这强制每个项目与容器(<ul>)一样宽。结果,内容将溢出其容器,视口将水平滚动。

css
li {
  list-style-type: none;
  background-color: #eeeeee;
  border: 1px solid #dddddd;
  padding: 20px;

  flex: 0 0 100%;
}

li:nth-child(even) {
  background-color: cyan;
}

此外,每个偶数编号的列表项通过 :nth-child() 被赋予了不同的背景颜色,以便更容易地看到滚动效果。

在列表上设置滚动捕捉

在本节中,我们将在 <ul> 上设置一个 overflow 值,将其变成一个滚动容器,然后应用 CSS 滚动捕捉,使列表在内容滚动时捕捉到每个列表项的中心。

<ul> 上设置 overflow-x 值为 scroll,使其内容在列表内水平滚动,而不是整个视口滚动。然后使用 CSS 滚动捕捉来捕捉到每个“页面”——设置 scroll-snap-type 值为 x mandatory,使列表成为一个滚动捕捉容器。关键字 x 会使容器的捕捉目标在水平方向上被捕捉,而关键字 mandatory 意味着容器在滚动动作结束时总是会捕捉到一个捕捉目标。

css
ul {
  overflow-x: scroll;
  scroll-snap-type: x mandatory;
}

接下来,在列表项上设置 scroll-snap-align 值为 center,这样当列表滚动时,它会捕捉到每个列表项的中心。

css
li {
  scroll-snap-align: center;
}

到目前为止显示的代码呈现如下:

尝试通过滑动或使用滚动条来滚动列表,以查看滚动捕捉的效果。无论你在哪里结束滚动动作,一个项目总是会“捕捉”到位。

注意: 使用 CSS 轮播特性并非强制要求使用 CSS 滚动捕捉。但是,包含了滚动捕捉的轮播效果要好得多。如果没有滚动捕捉,滚动按钮和标记将不太可能在页面之间干净地导航,结果会不尽如人意。

创建滚动按钮

在本节中,我们将在示例中添加“上一个”和“下一个”滚动按钮,以提供一个在轮播页面之间导航的工具。这是通过使用 ::scroll-button() 伪元素实现的。

::scroll-button() 伪元素仅当其 content 属性被设置为除 none 之外的值时,才会在滚动容器内部生成按钮。每个 ::scroll-button() 代表一个滚动按钮,其滚动方向由选择器的参数指定。每个滚动容器最多可以生成四个滚动按钮,每个按钮将容器的内容向块轴或行内轴的开始或结束方向滚动。

你还可以指定一个 * 参数来用样式定位所有的 ::scroll-button() 伪元素。

首先,为所有滚动按钮设定一些基本样式,以及基于不同状态的样式。为键盘用户设置 :focus 样式是很重要的。此外,由于滚动按钮在无法再向该方向滚动时会自动设置为 disabled,我们使用 :disabled 伪类来定位这种状态。

css
ul::scroll-button(*) {
  border: 0;
  font-size: 2rem;
  background: none;
  color: black;
  opacity: 0.7;
  cursor: pointer;
}

ul::scroll-button(*):hover,
ul::scroll-button(*):focus {
  opacity: 1;
}

ul::scroll-button(*):active {
  translate: 1px 1px;
}

ul::scroll-button(*):disabled {
  opacity: 0.2;
  cursor: unset;
}

注意: 我们还为滚动按钮设置了 cursor 值为 pointer,以使其更明显地可以与之交互(这对通用用户体验认知无障碍性都是一种改进),并在滚动按钮处于 :disabled 状态时取消设置。

接下来,通过 content 属性为左、右滚动按钮设置适当的图标,这也是导致滚动按钮生成的原因:

css
ul::scroll-button(left) {
  content: "◄";
}

ul::scroll-button(right) {
  content: "►";
}

注意: 滚动按钮会自动获得一个适当的无障碍名称,因此辅助技术会正确地播报它们。例如,上述按钮具有一个隐式的 role,值为 button,它们的无障碍名称分别是“向左滚动”和“向右滚动”。

定位滚动按钮

我们已经创建了滚动按钮。现在我们将使用 CSS 锚点定位将它们相对于轮播进行定位。

首先,在列表上设置一个参考 anchor-name。接下来,每个滚动按钮的 position 设置为 absolute,其 position-anchor 属性设置为在列表上定义的相同参考名称,以将两者关联起来。

css
ul {
  anchor-name: --my-carousel;
}

ul::scroll-button(*) {
  position: absolute;
  position-anchor: --my-carousel;
}

为了实际定位每个滚动按钮,我们在它们的内边距属性上设置值。我们使用 anchor() 函数来定位按钮的指定边相对于轮播的边。在每种情况下,都使用 calc() 函数在按钮边缘和轮播边缘之间添加一些空间。例如,左滚动按钮的右边缘被定位在轮播左边缘向右 70 像素的位置。

css
ul::scroll-button(left) {
  right: calc(anchor(left) - 70px);
  bottom: calc(anchor(top) + 13px);
}

ul::scroll-button(right) {
  left: calc(anchor(right) - 70px);
  bottom: calc(anchor(top) + 13px);
}

加入滚动按钮的代码后,我们得到以下结果:

尝试按下“上一个”和“下一个”滚动按钮,看看页面是如何滚动的,同时遵循滚动捕捉的行为。还要注意,当列表滚动到内容开始处时,“上一个”按钮是如何自动禁用的,而当列表滚动到内容结束处时,“下一个”按钮是如何自动禁用的。

创建滚动标记

滚动标记是一组按钮,每个按钮都将轮播滚动到与其中一个内容页面相关的位置。它们提供了额外的导航工具,同时也指示了你在轮播页面中的进度。

在本节中,我们将向轮播添加滚动标记,这涉及三个主要功能:

  • scroll-marker-group 属性设置在滚动容器元素上。它需要被设置为一个非 none 的值,以便生成 ::scroll-marker-group 伪元素;它的值指定了滚动标记组在轮播的 Tab 顺序和布局盒顺序中的出现位置(但不是 DOM 结构)——before 将其放在开始处,滚动按钮之前,而 after 将其放在末尾。
  • ::scroll-marker-group 伪元素存在于滚动容器内部,用于将滚动标记作为一个整体进行收集、包含和布局。
  • ::scroll-marker 伪元素存在于滚动容器祖先元素的子元素和 ::column 片段内部,并代表它们的滚动标记。这些标记被收集在祖先元素的 ::scroll-marker-group 中以便进行布局。

首先,列表的 scroll-marker-group 属性被设置为 after,这样 ::scroll-marker-group 伪元素就会在焦点和布局盒顺序中被放置在列表的 DOM 内容之后;这意味着它位于滚动按钮之后:

css
ul {
  scroll-marker-group: after;
}

注意:或者,可以使用 scroll-target-group 从一个包含一组 <a> 元素的现有元素创建一个滚动标记组容器。

接下来,列表的 ::scroll-marker-group 伪元素使用 CSS 锚点定位相对于轮播进行定位,类似于滚动按钮,但它在轮播上水平居中,使用 justify-self 值为 anchor-center。该组使用 flexbox 进行布局,justify-content 值为 centergap20px,这样其子元素(::scroll-marker 伪元素)就会在 ::scroll-marker-group 内部居中,并且每个元素之间有间隙。

css
ul::scroll-marker-group {
  position: absolute;
  position-anchor: --my-carousel;
  top: calc(anchor(bottom) - 70px);
  justify-self: anchor-center;

  display: flex;
  justify-content: center;
  gap: 20px;
}

接下来,我们处理滚动标记本身的外观和感觉;它们可以像任何其他生成内容一样进行样式设置。需要注意的是,我们需要为 content 属性设置一个非 none 的值,以便实际生成滚动标记。我们还设置了一些基本样式,使标记显示为带轮廓的圆形:

css
li::scroll-marker {
  content: "";
  width: 16px;
  height: 16px;
  background-color: transparent;
  border: 2px solid black;
  border-radius: 50%;
}

注意: 生成的内容默认是内联的;我们可以对我们的滚动标记应用 widthheight,因为它们是作为 flex 项目进行布局的。

最后,对于本节,使用 :target-current 伪类来选择与当前可见“页面”相对应的滚动标记,突出显示用户在内容中滚动的进度。在这种情况下,我们将 background-color 设置为 black,使其样式为一个实心圆。

css
li::scroll-marker:target-current {
  background-color: black;
}

注意: 当使用 scroll-marker-group 属性在滚动容器上创建滚动标记组容器时,滚动容器会以 tablist/tab 的语义进行渲染。你可以用键盘 Tab 到它,然后使用左、右(或上、下)光标键在不同的“页面”之间移动,这也会按预期改变相关滚动标记和滚动按钮的状态。滚动标记也可以像预期的那样正常地通过 Tab 键进行切换。

所有上述代码结合在一起,创建了以下结果:

自上一个实时示例以来,已经添加了滚动标记——尝试按下它们直接跳转到每个页面。请注意当前标记是如何高亮的,这样你就可以看到你在分页中的位置。也请尝试使用 Tab 键切换到滚动标记组,然后使用光标键循环浏览每个页面。

你还可以通过向左和向右滑动、拖动滚动条或按下滚动按钮来在页面之间导航。

第二个示例是一个每页有多个项目的轮播,它同样包含了滚动按钮滚动标记用于在页面间导航。这个示例也是响应式的——根据视口的宽度,每个页面上会出现不同数量的项目。

这个示例与单页轮播示例非常相似,不同之处在于它不使用 flexbox 进行布局,而是使用 CSS 多列布局::column 伪元素来创建跨越轮播整个宽度的任意列,这些列可能包含多个项目。

使用这种方法,我们可以确保如果视口变大或变小,而项目大小保持不变,我们永远不会有部分项目显示在滚动端口的边缘之外。在这种情况下,滚动标记是按列在滚动容器片段上创建的,而不是按项在子元素上创建的。

HTML 与上一个示例非常相似,只是列表项的数量要多得多,而且由于一次会显示多个项目,我们将其标记为项目而不是页面:

html
...
  <li>
    <h2>Item 1</h2>
  </li>
...

这个示例的 CSS 也非常相似,除了在以下部分中解释的规则外。

这个示例使用 CSS 多列布局,而不是 flexbox,来布局轮播项目。columns 值为 1 强制每列都占据容器的整个宽度,内容一次显示一列。还应用了 text-align 值为 center,强制内容与列表中心对齐。

css
ul {
  width: 100vw;
  height: 300px;
  padding: 10px;

  overflow-x: scroll;
  scroll-snap-type: x mandatory;

  columns: 1;
  text-align: center;
}

我们为列表项提供了基本的盒子样式,然后应用布局样式,以允许一个或多个项目适应单个内容列,具体取决于视口宽度。随着列表变宽或变窄,数量会动态变化。

css
li {
  list-style-type: none;

  display: inline-block;
  height: 100%;
  width: 200px;

  background-color: #eeeeee;
  border: 1px solid #dddddd;
  padding: 20px;
  margin: 0 10px;

  text-align: left;
}

li:nth-child(even) {
  background-color: cyan;
}

关键的布局属性如下:

  • display 值为 inline-block 被设置为强制列表项并排排列,并使列表水平滚动。
  • 为它们设置了一个 200px 的绝对 width 来控制它们的大小,这意味着一个或多个项目将适应一个随着视口宽度增长和收缩的列。
  • 为它们设置了 text-align 值为 left,以覆盖父容器上设置的 text-align: center,因此项目内容将左对齐。

scroll-snap-align 属性现在设置在 ::column 伪元素上——这些伪元素代表由 columns 属性生成的内容列——而不是列表项。我们想要捕捉到每个完整的列,而不是每个单独的列表项,每次滚动操作都显示所有新项目。

css
ul::column {
  scroll-snap-align: center;
}

列滚动标记

在这个示例中,用于创建滚动标记的 CSS 与上一个示例几乎相同,只是选择器不同——滚动标记是在生成的 ::column 伪元素上创建的,而不是在列表项上。注意我们在这里包含了两个伪元素来在生成的列上生成滚动标记。

css
ul::column::scroll-marker {
  content: "";
  width: 16px;
  height: 16px;
  background-color: transparent;
  border: 2px solid black;
  border-radius: 10px;
}

ul::column::scroll-marker:target-current {
  background-color: black;
}

响应式轮播的渲染效果如下:

尝试通过向左和向右滑动、使用滚动条、按下滚动按钮以及按下滚动标记在不同页面之间导航。功能与单页 flexbox 示例类似,不同的是现在每个导航位置都有多个列表项;滚动标记设置在可能包含多个项目的列片段上,而不是每个项目上。

此外,尝试调整屏幕宽度,你会看到列表内能容纳的列表项数量发生了变化——因此生成的列数也发生了变化。随着列数的变化,滚动标记的数量会动态更新,以便每个列都在滚动标记组中有所代表。

另见