构建面包屑导航组件

关于如何构建响应式且易于访问的面包屑导航组件以供用户浏览您的网站的基础概述。

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

演示

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

概述

面包屑导航组件显示用户在网站层次结构中的位置。这个名称来源于 汉赛尔和格蕾特,他们在黑暗的树林中撒下了面包屑,并通过追踪面包屑找到了回家的路。

这篇文章中的面包屑导航不是标准的面包屑导航,它们是类似面包屑导航的。它们通过将兄弟页面放入带有 <select> 的导航中来提供额外的功能,从而实现多层级的访问。

背景用户体验

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

此面包屑导航组件应使用户能够快速准确地浏览此信息层次结构;跳过分支并选择页面。

信息架构

我发现以集合和项目的角度来思考很有帮助。

集合

集合是可供选择的选项数组。从这篇文章的面包屑导航原型的主页来看,集合包括 FPS、RPG、格斗、地下城探索、体育和解谜。

项目

视频游戏是一个项目,如果特定集合代表另一个集合,则它也可以是一个项目。例如,RPG 是一个项目,也是一个有效的集合。当它是一个项目时,用户位于该集合页面上。例如,他们位于 RPG 页面上,该页面显示 RPG 游戏列表,包括额外的子类别 AAA、Indie 和 Self Published。

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

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

您的应用或网站将具有自定义的信息架构 (IA),从而创建不同的多维数组,但我希望集合着陆页和层次结构遍历的概念也能融入到您的面包屑导航中。

布局

标记

好的组件始于适当的 HTML。在接下来的部分中,我将介绍我的标记选择以及它们如何影响组件的整体效果。

深色和浅色方案

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

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

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

使用 <nav> 元素进行站点导航是合适的,它具有 ARIA navigation 角色的隐式属性。在测试中,我注意到拥有 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>

DevTools showing a rendered SVG use element.

定义一次,随意使用多次,页面性能影响最小,且样式灵活。请注意,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 上看到它位于最前面和中心位置。一个属性为许多用户提供了按钮上下文。

Screenshot with the invisible select element being hovered and its
contextual tooltip showing.

分隔符装饰

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

分隔符是可选的,仅添加一个分隔符效果也很好(请参阅上面视频中的第三个示例)。然后,我为每个分隔符都赋予了 aria-hidden="true",因为它们是装饰性的,而不是屏幕阅读器需要宣布的内容。

接下来介绍的 gap 属性使这些分隔符的间距设置变得简单。

样式

由于颜色使用系统颜色,因此样式主要由间距和堆叠构成!

布局方向和流

DevTools showing breadcrumb nav alignment with a its flexbox overlay
feature.

主导航元素 nav.breadcrumbs 设置了一个作用域自定义属性供子元素使用,并在其他方面建立了水平垂直对齐的布局。这确保了面包屑导航、分隔符和图标对齐。

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

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

One breadcrumb shown vertically aligned with flexbox overlays.

每个 .crumb 也建立了一个水平垂直对齐的布局,并带有一些间距,但专门针对其链接子元素并指定了样式 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 组件使用网格来堆叠 SVG 图标和一个“几乎不可见”的 <select> 元素。

Grid DevTools shown overlaying a button where the row and column are both
named stack.

.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;
  }}
}

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

  • 带有 overscroll 包含的水平滚动。
  • 水平滚动内边距。
  • 最后一个面包屑导航上的一个捕捉点。这意味着在页面加载时,第一个面包屑导航会捕捉并显示在视图中。
  • 从 Safari 中删除捕捉点,Safari 在水平滚动和捕捉效果组合方面存在问题。

媒体查询

针对较小视口的一个细微调整是隐藏“首页”标签,仅保留图标

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

Side by side of the breadcrumbs with and without a home label, for
comparison.

无障碍功能

动画

此组件中没有太多动画,但是通过将过渡效果包裹在 prefers-reduced-motion 检查中,我们可以防止不必要的动画。

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

其他样式都不需要更改,悬停和焦点效果在没有 transition 的情况下也很棒且有意义,但如果允许动画,我们将为交互添加微妙的过渡效果。

JavaScript

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

JavaScript 需要处理两个关键的用户体验措施:选择已更改和防止过早触发 <select> change 事件。

由于使用了 <select> 元素,因此需要防止过早触发事件。在 Windows Edge 和可能还有其他浏览器中,当用户使用键盘浏览选项时,select changed 事件会触发。这就是我称之为“过早”的原因,因为用户只是伪选择了该选项,例如悬停或焦点,但尚未通过 enterclick 确认选择。过早触发的事件使此组件类别更改功能变得不可访问,因为打开选择框并简单地浏览项目会触发事件并更改页面,而用户尚未准备好。

更好的 <select> changed 事件

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 上构建的所有方法。创建一个演示,在 Twitter 上给我发送链接,我会将其添加到下面的社区混音版块!

社区混音版