构建“标签页”组件

关于如何构建类似于 iOS 和 Android 应用中的标签页组件的基本概述。

在这篇博文中,我想分享关于构建适用于 Web 的标签页组件的想法 响应式、支持多种设备输入并可跨浏览器运行。 试用演示版

<ph type="x-smartling-placeholder">
</ph>
演示

如果你更喜欢视频,可以参阅此博文的 YouTube 版本:

概览

标签页是设计系统的常见组件,但可以有多种形状和 表单。最初,桌面标签页是基于 <frame> 元素构建的,现在我们有了 基于物理特性为内容添加动画效果的黄色移动组件。 它们都想方设法做同样的事情:节省空间。

如今,标签页用户体验的基本要素就是按钮导航区域 用于控制内容在显示框架中的可见性。许多不同的 这些内容区域共用同一个空间,但会根据不同的内容有条件地 按钮。

<ph type="x-smartling-placeholder">
</ph> 由于网络对组件概念应用的样式五花八门,因此拼图会很混乱 <ph type="x-smartling-placeholder">
</ph> 过去 10 年标签页组件网页设计风格的拼贴图

网络策略

总而言之,我发现这个组件的构建非常简单,这要归功于 几项重要的 Web 平台功能:

  • scroll-snap-points,可让您顺畅地通过滑动和键盘与以下对象进行交互: 相应的滚动停止位置
  • 通过网址哈希实现深层链接 浏览器处理的页内滚动锚定和共享支持
  • 屏幕阅读器支持 <a>id="#hash" 元素标记
  • prefers-reduced-motion,用于启用淡入淡出过渡和即时效果 页内滚动
  • 草稿版 @scroll-timeline Web 功能,用于动态添加下划线和 更改所选标签页的颜色

HTML

从根本上说,用户体验就是:点击链接,让网址代表嵌套式页面 页面状态,然后看到内容区域在浏览器滚动到 匹配元素。

其中包含一些结构化内容成员:链接和 :target。周三 需要一个 <nav> 非常适合的链接列表,以及一个 <article> 列表 元素,这是 <section> 的理想之选。每个链接哈希都会匹配一个部分 让浏览器通过锚定滚动网页

<ph type="x-smartling-placeholder">
</ph>
用户点击链接按钮后,在聚焦的内容中滑动

例如,点击链接会自动聚焦于以下部分中的 :target 文章: Chrome 89,无需 JS。然后,用户可以滚动浏览文章内容, 输入设备它是免费内容,如 标记。

我使用以下标记来整理标签页:

<snap-tabs>
  <header>
    <nav>
      <a></a>
      <a></a>
      <a></a>
      <a></a>
    </nav>
  </header>
  <section>
    <article></article>
    <article></article>
    <article></article>
    <article></article>
  </section>
</snap-tabs>

我可以使用以下代码在 <a><article> 元素之间建立连接: hrefid 属性,如下所示:

<snap-tabs>
  <header>
    <nav>
      <a href="#responsive"></a>
      <a href="#accessible"></a>
      <a href="#overscroll"></a>
      <a href="#more"></a>
    </nav>
  </header>
  <section>
    <article id="responsive"></article>
    <article id="accessible"></article>
    <article id="overscroll"></article>
    <article id="more"></article>
  </section>
</snap-tabs>

接下来,我在文章中加入了混合的洛勒姆元素,并用不同的颜色标记链接 混合长度和图片形式的标题。有了合适的内容后,我们就可以开始 布局。

滚动布局

此组件中有 3 种不同类型的滚动区域:

  • 导航 (粉色)是水平导航 可滚动
  • 内容区域(蓝色)处于水平状态 可滚动
  • 每个报道项(绿色)垂直显示 可滚动。
3 个彩色方框,带有与颜色匹配的方向箭头,这些箭头勾勒出滚动区域并指示滚动的方向。

滚动涉及到两种不同类型的元素:

  1. 窗口
    一个具有已定义尺寸的框,其中overflow 属性样式。
  2. 超大 Surface
    在此布局中,它是列表容器:nav 链接、版块文章和文章内容。

<snap-tabs>”布局

我选择的顶级布局是 flex (Flexbox)。我已将方向设为 column,因此标题和部分垂直排序。这是我们推出的第一个 滚动窗口,它会隐藏所有内容,同时隐藏溢出。标头和 部分很快将采用滚动,作为单个区域。

HTML
<snap-tabs>
  <header></header>
  <section></section>
</snap-tabs>
CSS
  snap-tabs {
  display: flex;
  flex-direction: column;

  /* establish primary containing box */
  overflow: hidden;
  position: relative;

  & > section {
    /* be pushy about consuming all space */
    block-size: 100%;
  }

  & > header {
    /* defend against 
needing 100% */ flex-shrink: 0; /* fixes cross browser quarks */ min-block-size: fit-content; } }

再次回到彩色的 3 滚动图:

  • <header> 现已准备好成为 (粉色) 滚动容器。
  • <section> 已准备好成为(蓝色)滚动 容器。

我在下面突出显示的这些帧 VisBug帮助我们查看窗口 创建的所有滚动容器

标题和部分元素上带有艳粉色叠加层,勾勒出它们在组件中占用的空间

标签页 <header> 布局

下一个布局几乎相同:我使用 flex 来创建垂直排序。

HTML
<snap-tabs>
  <header>
    <nav></nav>
    <span class="snap-indicator"></span>
  </header>
  <section></section>
</snap-tabs>
CSS
header {
  display: flex;
  flex-direction: column;
}

.snap-indicator 应与链接组沿水平方向移动, 此标题布局可帮助您做好万全准备。此处没有绝对定位元素!

nav 和 span.indicator 元素上有艳粉色叠加层,勾勒出它们在组件中所占空间

接下来是滚动样式原来我们可以分享滚动样式 两个水平滚动区域(标题和部分)之间,因此我构建了一个实用程序, 类 .scroll-snap-x

.scroll-snap-x {
  /* browser decide if x is ok to scroll and show bars on, y hidden */
  overflow: auto hidden;
  /* prevent scroll chaining on x scroll */
  overscroll-behavior-x: contain;
  /* scrolling should snap children on x */
  scroll-snap-type: x mandatory;

  @media (hover: none) {
    scrollbar-width: none;

    &::-webkit-scrollbar {
      width: 0;
      height: 0;
    }
  }
}

每个元素需要在 x 轴上溢出,滚动包含条件来阻止滚动,处于隐藏状态 为触摸设备设置滚动条,最后使用滚动贴靠功能锁定内容 演示区域。键盘标签页顺序易于访问,并且提供了任何互动指南 能够自然地专注滚动贴靠容器也采用了很棒的轮播样式 与键盘进行的互动。

标签页标题 <nav> 布局

导航链接需要垂直布局,不要换行 每个链接项都应与滚动条扣容器对齐。Swift 适用于 2021 CSS!

HTML
<nav>
  <a></a>
  <a></a>
  <a></a>
  <a></a>
</nav>
CSS
  nav {
  display: flex;

  & a {
    scroll-snap-align: start;

    display: inline-flex;
    align-items: center;
    white-space: nowrap;
  }
}

每个链接的样式和大小本身,因此导航布局只需指定 导航项具有独特的宽度,因而可在标签页之间转换 因为指示器会根据新的目标调整自身宽度,所以非常有趣。取决于 元素是否在此处,浏览器是否会呈现滚动条。

导航栏的 a 元素上有艳粉色叠加层,表示它们在组件中占据的空间以及它们溢出的位置

标签页 <section> 布局

此版块属于弹性商品,需要占据主要的空间。它 还需要为要放置的文章创建列。再次快速 适用于 CSS 2021!block-size: 100% 会拉伸此元素,以填充 那么对于自己的布局,它会创建一系列 宽度为 100% 且宽度为父项的列。百分比在这里效果很好 因为我们已经为父级设定了强有力的约束条件。

HTML
<section>
  <article></article>
  <article></article>
  <article></article>
  <article></article>
</section>
CSS
  section {
  block-size: 100%;

  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 100%;
}

这就好比是在说“以一种强迫的方式尽可能垂直展开” (请注意,我们将标头设置为 flex-shrink: 0,这是 展开推移),用于为一组全高列设置行高。通过 auto-flow 样式指示网格始终以水平方向排列子项 没有换行,这正是我们想要的溢出父窗口。

文章元素上叠加了艳粉色,勾勒出它们在组件中占据的空间以及它们溢出的位置

我觉得这些东西有时很难包起来!此部分元素为 同时也创建了一组箱子。我希望这些视觉元素 解释对我有所帮助。

标签页 <article> 布局

用户应能够滚动文章内容,且滚动条应 只有在溢出时才会显示。这些文章元素整齐地 排名。它们同时是滚动父级和滚动子级。通过 浏览器确实会处理一些棘手的触摸、鼠标和键盘互动

HTML
<article>
  <h2></h2>
  <p></p>
  <p></p>
  <h2></h2>
  <p></p>
  <p></p>
  ...
</article>
CSS
article {
  scroll-snap-align: start;

  overflow-y: auto;
  overscroll-behavior-y: contain;
}

我选择让报道在父级滚动条中贴靠。我真的很喜欢 导航链接项和文章元素如何与 inline-start 对齐 滚动容器的效果看起来和感觉和谐的 关系。

文章元素及其子元素上带有艳粉色叠加层,勾勒出它们在组件中占据的空间以及它们溢出的方向

文章是一个网格子项,其大小已预先确定为视口 我们希望提供滚动用户体验的方面也就是说,不需要指定高度或宽度 因此我需要定义其溢出方式。我将“overflow-y”设为“auto” 然后利用便捷的滚动行为来捕获滚动互动 属性。

回顾 3 个滚动区域

下方,我在系统设置中选择了“始终显示滚动条”。我觉得 打开此设置后,布局功能也就显得更为重要 查看布局和滚动编排

这 3 个滚动条已设置为显示,现在会占用布局空间,而且我们的组件仍然看起来非常棒

我认为在此组件中看到滚动条边线有助于清晰地显示 滚动区域的位置、支持的方向以及滚动区域的方式 相互交流。考虑一下每个滚动窗口框架的柔性或 grid 父项添加到布局中。

开发者工具可以帮助我们直观地了解这一点:

<ph type="x-smartling-placeholder">
</ph> 滚动区域具有网格和 Flexbox 工具叠加层,概述了它们在组件中占用的空间以及溢出的方向 <ph type="x-smartling-placeholder">
</ph> Chromium Devtools,显示充满锚点元素的 Flexbox 导航元素布局, 包含报道元素的网格部分布局,以及报道 所有段落和一个标题元素。

滚动布局已完成:贴靠、可深层链接、键盘 可访问性。为提升用户体验、打造风格和打造愉悦的用户体验奠定坚实基础。

功能亮点

在调整大小的过程中,滚动对齐的子节点会保持锁定位置。这意味着 JavaScript 不需要在设备旋转或浏览器上呈现任何内容 调整大小。在 Chromium 开发者工具中试用设备 Mode由 选择除自适应以外的任何模式,然后调整设备框架的大小。 请注意,该元素始终显示在视图中,并与其内容一起锁定。这是 可用,因为 Chromium 更新了其实现以符合规范。以下是 相关的博文

动画

此处动画工作的目的是明确将互动与界面相关联 反馈。这有助于引导或协助用户(希望) 让用户能顺畅地发现所有内容。我会有目的地添加动态效果 。用户现在可以指定其动作 偏好设置, 而且我非常喜欢在我的界面中根据他们的偏好做出回应。

我要将标签下划线与报道滚动位置相关联。“贴靠”功能不 它还可以锚定动画的开始和结束 这会保留 <nav>,其作用类似于 迷你地图,与内容相关联。 我们将从 CSS 和 JS 检查用户的动作偏好设置。这里有 几个值得考虑的好地方!

滚动行为

有机会增强 :targetelement.scrollIntoView()。默认情况下,它是即时的。浏览器只会将 滚动位置。如果我们要过渡到该滚动位置, 而不是眨眼的那一刻?

@media (prefers-reduced-motion: no-preference) {
  .scroll-snap-x {
    scroll-behavior: smooth;
  }
}

由于我们在此处引入运动,以及用户无法控制的运动 (与滚动一样),仅当用户在滚动浏览模式时 其操作系统的动作减少。这样,我们就只 并制作一些动画效果。

标签页指示符

此动画的目的是帮助将指示器与状态相关联 内容。我决定为用户提供淡入淡出的border-bottom样式 以及一个滚动链接的滑动 + 颜色淡入淡出动画 适合乐于运动的用户。

在 Chromium Devtools 中,我可以切换偏好设置,并演示 2 不同的过渡风格在构建这款游戏的过程中,我收获了无穷乐趣。

@media (prefers-reduced-motion: reduce) {
  snap-tabs > header a {
    border-block-end: var(--indicator-size) solid hsl(var(--accent) / 0%);
    transition: color .7s ease, border-color .5s ease;

    &:is(:target,:active,[active]) {
      color: var(--text-active-color);
      border-block-end-color: hsl(var(--accent));
    }
  }

  snap-tabs .snap-indicator {
    visibility: hidden;
  }
}

如果用户更喜欢减少动作,我会隐藏 .snap-indicator,因为我不 您再也不需要它了然后,将其替换为 border-block-end 样式和 transition。另请注意,在标签页交互中,活动的导航项 只突出显示了品牌下划线,但其文本颜色也较深。通过 活动元素具有较高的文字色彩对比度和明亮的背光强调效果。

多出几行 CSS 就会让人感觉自己被看到(从某种意义上说, 我们会谨慎考虑他们的动作偏好)。我很喜欢。

@scroll-timeline

在上一部分中,我向大家介绍了如何处理动态淡入淡出 在本节中,我将展示如何将指示器和 滚动区域。接下来我们要介绍一些有趣的实验性内容。希望你 我和我一样兴奋

const { matches:motionOK } = window.matchMedia(
  '(prefers-reduced-motion: no-preference)'
);

我首先在 JavaScript 中检查用户的动作偏好设置。如果 这是 false,这意味着用户更喜欢减少动作,则我们不会运行任何 用于关联动作效果的滚动效果

if (motionOK) {
  // motion based animation code
}

在撰写本文时,浏览器支持 @scroll-timeline 为无。这是一个 规范草稿,其中仅包含 实验性实现不过它有一个 polyfill 演示。

ScrollTimeline

CSS 和 JavaScript 都可以创建滚动时间轴 JavaScript,以便我可以在动画中使用实时元素测量值。

const sectionScrollTimeline = new ScrollTimeline({
  scrollSource: tabsection,  // snap-tabs > section
  orientation: 'inline',     // scroll in the direction letters flow
  fill: 'both',              // bi-directional linking
});

我想让一个元素跟随另一个对象的滚动位置, ScrollTimeline 我定义了滚动链接的驱动因素 scrollSource。 通常,Web 上的动画会针对全局时间帧 tick 运行,但具有 内存中的自定义 sectionScrollTimeline,我可以更改所有这些对象。

tabindicator.animate({
    transform: ...,
    width: ...,
  }, {
    duration: 1000,
    fill: 'both',
    timeline: sectionScrollTimeline,
  }
);

在介绍动画的主帧之前,我觉得有必要先确定 指出滚动的关注者 tabindicator 将基于 也就是版块的滚动条这样就完成了关联 缺少添加动画效果的有状态点,也称为 关键帧。

动态关键帧

你可以使用一种非常强大的纯声明式 CSS 方法 @scroll-timeline,但我选择使用的动画过于动感。我们没有 能够在 auto 宽度之间转换,而且无法动态创建 基于子元素长度的关键帧数量。

不过,JavaScript 知道如何获取这些信息,因此,我们会对 并在运行时获取计算值:

tabindicator.animate({
    transform: [...tabnavitems].map(({offsetLeft}) =>
      `translateX(${offsetLeft}px)`),
    width: [...tabnavitems].map(({offsetWidth}) =>
      `${offsetWidth}px`)
  }, {
    duration: 1000,
    fill: 'both',
    timeline: sectionScrollTimeline,
  }
);

对于每个 tabnavitem,解构 offsetLeft 位置并返回一个字符串 并将其用作 translateX 值。此操作会为 动画。对宽度也是如此,系统会询问每个广告素材的动态宽度是多少 然后将其用作关键帧值

下面是基于我的字体和浏览器偏好设置的输出示例:

TranslateX 关键帧:

[...tabnavitems].map(({offsetLeft}) =>
  `translateX(${offsetLeft}px)`)

// results in 4 array items, which represent 4 keyframe states
// ["translateX(0px)", "translateX(121px)", "translateX(238px)", "translateX(464px)"]

宽度关键帧:

[...tabnavitems].map(({offsetWidth}) =>
  `${offsetWidth}px`)

// results in 4 array items, which represent 4 keyframe states
// ["121px", "117px", "226px", "67px"]

总结一下策略,标签指示器现在跨 4 个关键帧以动画形式呈现 具体取决于版块滚动条的滚动贴靠位置。贴靠点 明确划分各个关键帧 动画的同步感觉。

活动标签页和非活动标签页会与 VisBug 叠加层一起显示,并同时显示两者的合格对比度得分

用户通过互动来驱动动画,查看宽度和 指示符的位置从一个部分移动到另一个部分, 完美呈现。

您可能没有注意到,但作为角色的这种改变,我感到非常自豪。 选中的导航项就会变为选中状态

当未选中的浅灰色突出显示时, 对比度越高。文本颜色的转换颜色很常见,例如悬停时 选中后,它会在滚动时进行下一层转换 与下划线指示符同步

具体操作步骤如下:

tabnavitems.forEach(navitem => {
  navitem.animate({
      color: [...tabnavitems].map(item =>
        item === navitem
          ? `var(--text-active-color)`
          : `var(--text-color)`)
    }, {
      duration: 1000,
      fill: 'both',
      timeline: sectionScrollTimeline,
    }
  );
});

每个标签页导航链接都需要这种新的彩色动画来跟踪相同的滚动 作为下划线指示符我使用与之前相同的时间轴: 其作用是在滚动时发出一个 tick,我们可以在任何类型的 所需的动画效果和之前一样,我循环创建了 4 个关键帧 颜色。

[...tabnavitems].map(item =>
  item === navitem
    ? `var(--text-active-color)`
    : `var(--text-color)`)

// results in 4 array items, which represent 4 keyframe states
// [
  "var(--text-active-color)",
  "var(--text-color)",
  "var(--text-color)",
  "var(--text-color)",
]

颜色为 var(--text-active-color) 的关键帧突出显示了该链接; 否则会采用标准文本颜色其中的嵌套循环使其相对 直接,因为外圈是每个导航项,内圈是每个导航项 navitem 的个人关键帧。我检查外圈元素是否与 并使用此内部循环来获知它在何时被选中。

写这本书我收获了很多乐趣。喜欢得不得了

更多 JavaScript 增强功能

值得一提的是,我演示的核心功能 JavaScript。既然如此,我们还是看看 可用。

深层链接更像是一个移动术语,但我认为深层链接 在这里,您可以直接分享指向标签页内容的网址。通过 浏览器将在页内导航至与网址哈希值相匹配的 ID。我找到了 此 onload 处理程序会跨平台生效。

window.onload = () => {
  if (location.hash) {
    tabsection.scrollLeft = document
      .querySelector(location.hash)
      .offsetLeft;
  }
}

滚动结束同步

我们的用户并不总是点击或使用键盘,有时他们 用户应能够自由滚动浏览当版块滚动条停止运行时 需要与顶部导航栏中的哪个位置相匹配。

等待滚动结束的方法如下: js tabsection.addEventListener('scroll', () => { clearTimeout(tabsection.scrollEndTimer); tabsection.scrollEndTimer = setTimeout(determineActiveTabSection, 100); });

每当滚动部分时,请清除部分超时(如果有), 然后创建一个新请求当各部分停止滚动时,请勿清除超时 并在静息 100 毫秒后触发。当它触发时,调用试图计算的函数 用户停止的位置

const determineActiveTabSection = () => {
  const i = tabsection.scrollLeft / tabsection.clientWidth;
  const matchingNavItem = tabnavitems[i];

  matchingNavItem && setActiveTab(matchingNavItem);
};

假设滚动已贴靠,当前滚动位置与宽度相同 应是整数,而不是小数。然后,我尝试 通过此计算索引从缓存中获取 navitem,如果找到 我发送匹配,让它开始生效

const setActiveTab = tabbtn => {
  tabnav
    .querySelector(':scope a[active]')
    .removeAttribute('active');

  tabbtn.setAttribute('active', '');
  tabbtn.scrollIntoView();
};

要设置活动标签页,首先请清除所有当前活动的标签页,然后 传入的导航项启用状态属性。对 scrollIntoView() 的调用 它与 CSS 进行了有趣的互动

.scroll-snap-x {
  overflow: auto hidden;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;

  @media (prefers-reduced-motion: no-preference) {
    scroll-behavior: smooth;
  }
}

在水平滚动贴靠实用工具 CSS 中 nested 媒体查询,它应用 smooth 滚动。JavaScript 可以自由地 调用将元素滚动到视图中,CSS 可以通过声明方式管理用户体验。 偶尔也会搭上她那可爱的小家伙。

总结

现在你知道我怎么做到的了,你会怎么做?!这样很有趣 组件架构!谁会制作在他们的 您最喜欢的框架是什么?🙂

让我们一起采用多样化的方法,学习所有在 Web 上构建应用的方法。 创建 Glitch发 Twitter 微博 我会将其添加到社区混剪视频中 部分。

社区混剪作品