构建媒体滚动条组件

关于如何为电视、手机、桌面设备等构建响应式水平滚动视图的基础概述。

在这篇文章中,我想分享关于如何为 Web 创建水平滚动体验的思考,这些体验应是极简的、响应式的、无障碍的,并且可以在浏览器和平台(如电视!)上运行。请尝试演示

演示

如果您喜欢视频,这里是这篇文章的 YouTube 版本

概述

我们将构建一个水平滚动布局,用于托管媒体或产品的缩略图。该组件最初是一个简单的 <ul> 列表,但通过 CSS 转换为令人满意且流畅的滚动体验,展示图像并将其对齐到网格。添加了 JavaScript 以促进漫游索引交互,帮助键盘用户跳过遍历 100 多个项目。此外,还使用了一个实验性媒体查询 prefers-reduced-data,将媒体滚动条转变为轻量级标题滚动条体验。

从无障碍标记开始

媒体滚动条仅由几个核心组件组成,即包含项目的列表。列表以最简单的形式,可以传遍世界各地,并被所有人清晰地理解。访问此页面的用户可以浏览列表并点击链接以查看项目。这是我们的无障碍基础。

使用 <ul> 元素交付列表

<ul class="horizontal-media-scroller">
  <li></li>
  <li></li>
  <li></li>
  ...
<ul>

使用 <a> 元素使列表项可交互

<li>
  <a href="#">
    ...
  </a>
</li>

使用 <figure> 元素语义化地表示图像及其标题

<figure>
  <picture>
    <img alt="..." loading="lazy" src="https://picsum.photos/500/500?1">
  </picture>
  <figcaption>Legends</figcaption>
</figure>

请注意 <img> 上的 altloading 属性。媒体滚动条的替代文本是一个 UX 机会,可以帮助为缩略图带来额外的上下文,或者在图像未加载时作为回退文本,或者为依赖屏幕阅读器等辅助技术的用户提供语音 UI。通过 符合标准的替代文本的五条黄金法则 了解更多信息。

loading 属性接受关键字 lazy,以此来表示只有当图像位于视口内时才应获取此图像源。这对于大型列表非常有用,因为用户只会下载他们滚动到视图中的项目的图像。

支持用户的配色方案偏好

使用 color-scheme 作为 <meta> 标记,向浏览器发出信号,表明您的页面需要浅色和深色这两种用户代理样式。这是一种免费的深色模式或浅色模式,具体取决于您的看法

<meta name="color-scheme" content="dark light">

meta 标记提供了最早的信号,因此如果用户具有深色主题偏好,浏览器可以选择深色默认画布颜色。这意味着站点页面之间的导航不会在加载之间闪烁白色画布背景。加载之间无缝的深色主题,对眼睛更友好。

Thomas Steiner 处了解更多信息,网址为 https://webdev.ac.cn/color-scheme/

添加内容

鉴于上述 ul > li > a > figure > picture > img 的内容结构,下一个任务是添加图像和标题以进行滚动。我已经在演示中填充了静态占位符图像和文本,但您可以随意从您喜欢的数据源提供支持。

使用 CSS 添加样式

现在是时候使用 CSS 将这个通用的内容列表变成一种体验了。Netflix、应用商店和更多站点及应用使用水平滚动区域来在视口中填充类别和选项。

创建滚动条布局

重要的是要避免在布局中截断内容或依赖省略号进行文本截断。许多电视机都有像这样的媒体滚动条,但经常会求助于省略内容。此布局不会!它还允许媒体内容覆盖列大小,使 1 个布局足够灵活以处理许多有趣的组合。

2
scrolling rows shown. One has no ellipsis, which means it's taller and each
title is fully legible. The other is shorter and many titles are cutoff with
ellipsis.

容器允许通过提供自定义属性作为默认大小来覆盖列大小。此网格布局对列大小有自己的看法,它仅管理间距和方向

.horizontal-media-scroller {
  --size: 150px;

  display: grid;
  grid-auto-flow: column;
  gap: calc(var(--gap) / 2); /* parent owned value for children to be relative to*/
  margin: 0;
}

然后,自定义属性被 <picture> 元素使用,以创建我们的基本纵横比:一个框

.horizontal-media-scroller {
  --size: 150px;

  display: grid;
  grid-auto-flow: column;
  gap: calc(var(--gap) / 2);
  margin: 0;

  & picture {
    inline-size: var(--size);
    block-size: var(--size);
  }
}

只需几个更小的样式,即可完成媒体滚动条的基本框架

.horizontal-media-scroller {
  --size: 150px;

  display: grid;
  grid-auto-flow: column;
  gap: calc(var(--gap) / 2);
  margin: 0;

  overflow-x: auto;
  overscroll-behavior-inline: contain;

  & > li {
    display: inline-block; /* removes the list-item bullet */
  }

  & picture {
    inline-size: var(--size);
    block-size: var(--size);
  }
}

设置 overflow<ul> 设置为允许通过其列表进行滚动和键盘导航,然后每个直接子元素 <li> 元素通过获得新的显示类型 inline-block 来删除其 ::marker

但是图像还没有响应式,并且会从它们内部的框中爆出来。使用一些尺寸、fit 和边框样式来驯服它们,并为它们在延迟加载时添加背景渐变

img {
  /* smash into whatever box it's in */
  inline-size: 100%;
  block-size: 100%;

  /* don't squish but do cover the space */
  object-fit: cover;

  /* soften the edges */
  border-radius: 1ex;
  overflow: hidden;

  /* if empty, show a gradient placeholder */
  background-image:
    linear-gradient(
      to bottom,
      hsl(0 0% 40%),
      hsl(0 0% 20%)
    );
}

滚动内边距

与页面内容对齐,加上边缘到边缘的滚动表面积,对于和谐且极简的组件至关重要。

为了实现与我们的排版和布局线对齐的边缘到边缘滚动布局,请使用与 scroll-padding 匹配的 padding

.horizontal-media-scroller {
  --size: 150px;

  display: grid;
  grid-auto-flow: column;
  gap: calc(var(--gap) / 2);
  margin: 0;

  overflow-x: auto;
  overscroll-behavior-inline: contain;

  padding-inline: var(--gap);
  scroll-padding-inline: var(--gap);
  padding-block: calc(var(--gap) / 2); /* make space for scrollbar and focus outline */
}

水平滚动内边距错误修复 上面展示了填充滚动容器应该有多么容易,但是它仍然存在兼容性问题(尽管已在 Chromium 91+ 中修复!)。请参阅 此处 了解一些历史记录,但简短的版本是,内边距并不总是会在滚动视图中被考虑在内。

A
box is highlighted on the inline-end side of the last list item, showing the
padding and element have the same width as to create the desired alignment.

为了欺骗浏览器将内边距放在滚动条的末尾,我将定位每个列表中的最后一个 figure,并附加一个伪元素,该伪元素是所需的内边距量。

.horizontal-media-scroller > li:last-of-type figure {
  position: relative;

  &::after {
    content: "";
    position: absolute;

    inline-size: var(--gap);
    block-size: 100%;

    inset-block-start: 0;
    inset-inline-end: calc(var(--gap) * -1);
  }
}

使用逻辑属性使媒体滚动条可以在任何书写模式和文档方向下工作。

滚动吸附

具有 overflow 的滚动容器可以通过一行 CSS 变成吸附视口,然后由子元素指定它们希望如何与该视口对齐。

.horizontal-media-scroller {
  --size: 150px;

  display: grid;
  grid-auto-flow: column;
  gap: calc(var(--gap) / 2);
  margin: 0;

  overflow-x: auto;
  overscroll-behavior-inline: contain;

  padding-inline: var(--gap);
  scroll-padding-inline: var(--gap);
  padding-block-end: calc(var(--gap) / 2);

  scroll-snap-type: inline mandatory;

  & figure {
    scroll-snap-align: start;
  }
}

焦点

此组件的灵感来自于它在电视、应用商店等平台上的广泛普及。许多视频游戏平台都使用与此类似的媒体滚动条作为其主要主屏幕布局。焦点在这里是一个巨大的 UX 时刻,而不仅仅是一个小的补充。想象一下,在沙发上使用遥控器使用此媒体滚动条,为这种交互提供一些小的增强

.horizontal-media-scroller a {
  outline-offset: 12px;

  &:focus {
    outline-offset: 7px;
  }

  @media (prefers-reduced-motion: no-preference) {
    & {
      transition: outline-offset .25s ease;
    }
  }
}

这会将焦点轮廓样式设置为距离框 7px,为其提供一些不错的空间。如果用户没有关于减少运动的运动偏好,则会过渡偏移量,从而为焦点事件提供微妙的运动。

漫游索引

游戏手柄和键盘用户在这些长长的滚动内容和选项列表中需要特别注意。解决此问题的常用模式称为 漫游索引。它是指当一个项目容器被键盘聚焦时,但一次只允许 1 个子元素保持焦点。这种一次聚焦一个项目的体验旨在允许绕过可能很长的项目列表,而不是按 tab 键 50 多次才能到达末尾。

演示的第一个滚动条中有 300 个项目。我们可以做得更好,而不是让他们遍历所有项目才能到达下一部分。

为了创建这种体验,JavaScript 需要观察键盘事件和焦点事件。我创建了 npm 上的一个小型的开源库,以帮助使这种用户体验易于实现。以下是如何将其用于 3 个滚动条

import {rovingIndex} from 'roving-ux';

rovingIndex({
  element: someElement
});

此演示查询文档中的滚动条,并为每个滚动条调用 rovingIndex() 函数。将 rovingIndex() 传递给元素以获得漫游体验,例如列表容器,以及目标查询选择器,以防焦点目标不是直接后代。

document.querySelectorAll('.horizontal-media-scroller')
  .forEach(scroller =>
    rovingIndex({
      element: scroller,
      target: 'a',
}))

要了解有关此效果的更多信息,请参阅开源库 roving-ux

纵横比

在撰写这篇文章时,aspect-ratio 的支持 在 Firefox 中位于标志之后,但在 Chromium 浏览器或机顶盒中可用。由于媒体滚动条网格布局仅指定方向和间距,因此大小可以在媒体查询中更改,该媒体查询的功能检查纵横比支持。渐进增强为一些更动态的媒体滚动条。

A
box with 4:4 aspect ratio is shown next to the other design ratios used of 16:9
and 4:3

@supports (aspect-ratio: 1) {
  .horizontal-media-scroller figure > picture {
    inline-size: auto; /* for a block-size driven ratio */
    aspect-ratio: 1; /* boxes by default */

    @nest section:nth-child(2) & {
      aspect-ratio: 16/9;
    }

    @nest section:nth-child(3) & {
      /* double the size of the others */
      block-size: calc(var(--size) * 2);
      aspect-ratio: 4/3;

      /* adjust size to fit more items into the viewport */
      @media (width <= 480px) {
        block-size: calc(var(--size) * 1.5);
      }
    }
  }
}

如果浏览器支持 aspect-ratio 语法,则媒体滚动条图片将升级为 aspect-ratio 大小。使用草案嵌套语法,每张图片都会根据它是第一行、第二行还是第三行来更改其纵横比。嵌套语法 还允许设置一些小的视口调整,就在其他大小逻辑旁边。

有了 CSS,随着该功能在更多浏览器引擎中可用,将呈现一个易于管理但更具视觉吸引力的布局。

首选减少数据

虽然下一项技术仅在 Canary位于标志之后 可用,但我还是想分享如何通过几行 CSS 节省大量页面加载时间和数据使用量。来自 level 5prefers-reduced-data 媒体查询允许询问设备是否处于任何数据减少状态,例如数据节省模式。如果是,我可以修改文档,在本例中,隐藏图像。

ALT_TEXT_HERE

figure {
  @media (prefers-reduced-data: reduce) {
    & {
      min-inline-size: var(--size);

      & > picture {
        display: none;
      }
    }
  }
}

内容仍然可以导航,但无需下载大型图像的成本。这是添加 prefers-reduced-data CSS 之前的网站

(7 个请求,131 毫秒内 100kb 的资源)

ALT_TEXT_HERE

这是添加 prefers-reduced-data CSS 之后的网站性能

ALT_TEXT_HERE

(71 个请求,1.07 秒内 1.2mb 的资源)

减少了 64 个请求,这应该是此浏览器标签页视口中 ~60 张图像(在宽屏显示器上进行的测试),页面加载速度提升了 ~80%,并且网络传输的数据减少了 10%。非常强大的 CSS。

结论

现在您知道我是如何做到的了,您会怎么做呢?!🙂

让我们使我们的方法多样化,并学习在 Web 上构建的所有方法。创建一个 Codepen 或托管您自己的演示,在 Twitter 上向我发送,我将把它添加到下面的“社区混音”部分。

源代码

社区混音

这里目前还没有内容!