构建面包屑导航组件

基本概述如何构建响应迅速的无障碍面包屑导航组件,以便用户浏览您的网站。

在这篇博文中,我想分享关于构建面包屑导航组件的想法。试用演示版

演示

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

概览

面包屑导航组件会显示用户在网站层次结构中的位置,其名称来自 Hansel 和 Gretel,他们在一些黑暗的树林中在其后方放置面包屑导航,他们能够通过向后跟踪面包屑导航找到回家的路。

本博文中的面包屑导航不是标准面包屑导航,而是类似于面包屑导航的面包屑导航。它们通过 <select> 将同级页面直接放置到导航中来提供其他功能,实现多层级访问。

后台用户体验

在上面的组件演示视频中,占位符类别是视频游戏的类型。此小路是通过导航以下路径创建的:home » rpg » indie » on sale,如下所示。

此面包屑导航组件应让用户能够在此信息层次结构之间移动;快速且准确地选择跳转分支和选择页面。

信息架构

我发现从合集和作品的角度考虑问题很有帮助。

集合

集合是指一组可供选择的选项。在本博文的面包屑导航原型的首页上,这些集合包括 FPS、RPG、brawler、地牢抓取工具、体育和益智游戏。

内容

视频游戏是一个商品,如果特定集合代表其他集合,那么该集合也可以作为该商品。例如,RPG 是一个商品和一个有效的集合。如果是项,则用户位于该集合页面上。例如,它们位于 RPG 页面上,该页面会显示 RPG 游戏列表,其中包括 AAA、独立游戏和自行发布等其他子类别。

用计算机科学的术语来说,此面包屑导航组件表示一个多维数组

const rawBreadcrumbData = {
  "FPS": {...},
  "RPG": {
    "AAA": {...},
    "indie": {
      "new": {...},
      "on sale": {...},
      "under 5": {...},
    },
    "self published": {...},
  },
  "brawler": {...},
  "dungeon crawler": {...},
  "sports": {...},
  "puzzle": {...},
}

您的应用或网站将采用自定义信息架构 (IA) 来创建不同的多维数组,但我希望集合着陆页和层次结构遍历这一概念也能在您的面包屑导航中实现。

布局

Markup

良好的组件是以适当的 HTML 开头。在下一部分中,我将说明我的标记选择,以及它们对整体组件的影响。

深色和浅色方案

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

上述代码段中的 color-scheme 元标记会告知浏览器:此页面需要浅色和深色浏览器样式。示例面包屑导航未包含这些配色方案的任何 CSS,因此面包屑导航将使用浏览器提供的默认颜色。

<nav class="breadcrumbs" role="navigation"></nav>

不妨将 <nav> 元素用于网站导航,后者具有隐式 ARIA 导航作用。 在测试中,我注意到,设置 role 属性会改变屏幕阅读器与该元素互动的方式,它实际上被读出为导航,因此我选择添加它。

图标

当页面上重复出现某个图标时,SVG <use> 元素意味着您可以定义一次 path,并将其用于图标的所有实例。这样可以防止相同的路径信息重复,从而导致文档变大,并可能出现路径不一致的情况。

若要使用此方法,请向页面添加隐藏的 SVG 元素,并将图标封装在具有唯一 ID 的 <symbol> 元素中:

<svg style="display: none;">

  <symbol id="icon-home">
    <title>A home icon</title>
    <path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
  </symbol>

  <symbol id="icon-dropdown-arrow">
    <title>A down arrow</title>
    <path d="M19 9l-7 7-7-7"/>
  </symbol>

</svg>

浏览器会读取 SVG HTML,将图标信息放入内存中,然后继续执行,直到网页的其余部分引用该 ID 再使用该图标,如下所示:

<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
  <use href="#icon-home" />
</svg>

<svg viewBox="0 0 24 24" width="24" height="24" aria-hidden="true">
  <use href="#icon-dropdown-arrow" />
</svg>

显示已渲染的 SVG 使用元素的开发者工具。

只需定义一次,即可无限次使用,同时网页性能影响极小,而且样式灵活。请注意,aria-hidden="true" 已添加到 SVG 元素中。对于只听内容的浏览者来说,这些图标并无用处,如果对这些用户隐藏图标,则会阻止他们添加不必要的噪音。

这正是传统面包屑导航与此组件中的导航方式的差异所在。通常,这只会是一个 <a> 链接,但我通过伪装选择添加了遍历用户体验。.crumb 类负责布置链接和图标,而 .crumbicon 负责将图标和选择元素堆叠在一起。我将其称为分屏链接,是因为它的功能与分屏按钮非常相似,但用于页面导航。

<span class="crumb">
  <a href="#sub-collection-b">Category B</a>
  <span class="crumbicon">
    <svg>...</svg>
    <select class="disguised-select" title="Navigate to another category">
      <option>Category A</option>
      <option selected>Category B</option>
      <option>Category C</option>
    </select>
  </span>
</span>

链接和一些选项没有什么特别之处,只是在简单的面包屑导航中增加了更多功能。向 <select> 元素添加 title 有助于屏幕阅读器用户了解按钮操作的相关信息。不过,它为其他所有人提供了相同的帮助,您会看到它在 iPad 上的醒目位置。一个属性可为许多用户提供按钮上下文。

一张屏幕截图,其中显示了悬停在不可见的选择元素及其上下文提示上。

分隔符装饰

<span class="crumb-separator" aria-hidden="true">→</span>

分隔符是可选的,只需添加一个分隔符,效果也不错(请参阅上述视频中的第三个示例)。然后,我为每个 aria-hidden="true" 指定了一种装饰性元素,它们不需要屏幕阅读器读出的内容。

下文将介绍 gap 属性,让各个标签的间距简单明了。

风格

由于颜色使用的是系统颜色,因此它主要是样式的间隙和堆叠!

布局方向和流程

显示面包屑导航导航对齐方式及其 flexbox 叠加层功能的开发者工具。

主要导航元素 nav.breadcrumbs 用于设置作用域自定义属性供子级使用,否则会建立水平垂直对齐的布局。这样可以确保路径、分隔线和图标对齐。

.breadcrumbs {
  --nav-gap: 2ch;

  display: flex;
  align-items: center;
  gap: var(--nav-gap);
  padding: calc(var(--nav-gap) / 2);
}

一个面包屑导航与 Flexbox 叠加层垂直对齐。

每个 .crumb 还会建立一个水平垂直对齐的布局并留出一定的间隙,但专门定位其 link 子级并指定样式 white-space: nowrap。这对于多字词面包屑导航至关重要,因为我们不希望它们出现在多行位置。在这篇博文的后面部分,我们将添加样式来处理此 white-space 属性导致的水平溢出。

.crumb {
  display: inline-flex;
  align-items: center;
  gap: calc(var(--nav-gap) / 4);

  & > a {
    white-space: nowrap;

    &[aria-current="page"] {
      font-weight: bold;
    }
  }
}

添加了 aria-current="page",以帮助当前页面链接脱颖而出。屏幕阅读器用户不仅可以清楚地看到链接指向当前页面,还可以直观地设置元素的样式,帮助视力正常的用户获得类似的用户体验。

.crumbicon 组件使用网格堆叠具有“几乎不可见”的 <select> 元素的 SVG 图标。

网格开发者工具显示叠加在一个按钮上,其中行和列都是已命名的堆栈。

.crumbicon {
  --crumbicon-size: 3ch;

  display: grid;
  grid: [stack] var(--crumbicon-size) / [stack] var(--crumbicon-size);
  place-items: center;

  & > * {
    grid-area: stack;
  }
}

<select> 元素是 DOM 中的最后一个元素,因此位于堆栈的顶部,并且可以交互。添加 opacity: .01 样式,以使元素仍然可用,从而生成一个与图标形状完美契合的选择框。这是在保持内置功能的同时自定义 <select> 元素的外观的好方法。

.disguised-select {
  inline-size: 100%;
  block-size: 100%;
  opacity: .01;
  font-size: min(100%, 16px); /* Defaults to 16px; fixes iOS zoom */
}

溢出式菜单

面包屑导航应能够代表很长的路径。我非常喜欢在适当的情况下允许内容以水平方式离开屏幕,而且我认为此面包屑导航组件符合要求。

.breadcrumbs {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  scroll-padding-inline: calc(var(--nav-gap) / 2);

  & > .crumb:last-of-type {
    scroll-snap-align: end;
  }

  @supports (-webkit-hyphens:none) { & {
    scroll-snap-type: none;
  }}
}

溢出样式设置了以下用户体验:

  • 包含滚动回弹的水平滚动。
  • 水平滚动内边距。
  • 最后一个面包屑导航有一个贴靠点。这意味着,在网页加载时,第一个面包屑导航会贴靠到视图内。
  • 从 Safari 中移除了与水平滚动和贴靠效果组合不兼容的贴靠点。

媒体查询

对于较小的视口,细微的调整是隐藏“主屏幕”标签,只留下图标:

@media (width <= 480px) {
  .breadcrumbs .home-label {
    display: none;
  }
}

并排显示面包屑导航(有和不带首页标签),以便进行比较。

无障碍功能

动作

此组件中没有很多动作,但通过将过渡效果封装在 prefers-reduced-motion 检查中,我们可以防止产生不必要的动作。

@media (prefers-reduced-motion: no-preference) {
  .crumbicon {
    transition: box-shadow .2s ease;
  }
}

所有其他样式无需更改,如果没有 transition,悬停和焦点效果会既棒又有意义,但如果运动可以接受,我们将为互动添加细微的过渡。

JavaScript

首先,无论您在网站或应用中使用哪种类型的路由器,当用户更改面包屑导航时,都需要更新网址并显示相应的页面。其次,为了实现用户体验标准化,请确保用户只浏览 <select> 选项时不会发生意外导航。

由 JavaScript 处理的两个关键用户体验措施:select 已更改,以及防止 <select> 更改事件触发。

由于使用了 <select> 元素,需要使用紧急事件防范功能。在 Windows Edge(可能还有其他浏览器)上,当用户使用键盘浏览选项时,会触发选定 changed 事件。这就是我将其称为“eager”的原因,因为用户只是伪选择了悬停或聚焦选项,但尚未通过 enterclick 确认选择。紧急事件会导致此组件类别更改功能无法访问,因为在用户尚未准备好的情况下,打开选择框并直接浏览项目会触发该事件并更改页面。

更改了<select>活动

const crumbs = document.querySelectorAll('.breadcrumbs select')
const allowedKeys = new Set(['Tab', 'Enter', ' '])
const preventedKeys = new Set(['ArrowUp', 'ArrowDown'])

// watch crumbs for changes,
// ensures it's a full value change, not a user exploring options via keyboard
crumbs.forEach(nav => {
  let ignoreChange = false

  nav.addEventListener('change', e => {
    if (ignoreChange) return
    // it's actually changed!
  })

  nav.addEventListener('keydown', ({ key }) => {
    if (preventedKeys.has(key))
      ignoreChange = true
    else if (allowedKeys.has(key))
      ignoreChange = false
  })
})

其策略是观察每个 <select> 元素上的键盘按下事件,并确定按下的键是导航确认(TabEnter)还是空间导航(ArrowUpArrowDown)。确定后,组件可以在 <select> 元素的事件触发时决定是等待还是返回。

总结

现在你已经知道我是怎么做的,希望你怎么办 ‽ 🙂?

下面,我们就来介绍一下我们的方法多样化,并了解在 Web 上构建网站的所有方法。 只需创建一个演示,点击 tweet me 链接,我就会将其添加到下方的“社区混剪”部分中!

社区混剪作品