构建面包屑导航组件

简要介绍如何构建便于用户浏览您网站的自适应面包屑导航组件。

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

演示

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

概览

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

这篇博文中的面包屑导航不是标准面包屑导航,而是类似于面包屑导航的面包屑导航。它们使用 <select> 将同级页面放在导航中,从而提供额外的功能,以实现多层级访问。

后台用户体验

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

此面包屑导航组件应让用户能够快速准确地浏览这些信息层次结构,从而实现跳转和选择页面。

信息架构

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

集合

集合是一系列可供选择的选项。从这篇博文的面包屑导航原型的首页开始,集合为 FPS、RPG、乱斗、地牢爬行、体育和益智游戏。

内容

视频游戏是一种商品,如果特定集合代表另一个集合,那么该商品也可以是一个商品。例如,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 使用元素的开发者工具。

只需定义一次,即可根据需要多次使用,同时最大限度地减少页面性能影响和灵活的样式。请注意,SVG 元素中添加了 aria-hidden="true"。对于只听到内容的用户浏览时,这些图标没有用处,因此向这些用户隐藏这些图标可以避免增加不必要的干扰。

这就是传统面包屑导航与此组件中的面包屑导航的区别。 通常,这只是一个 <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 上看到它位于最显眼的位置。一个属性可为许多用户提供按钮上下文。

一个屏幕截图,其中悬停了不可见的 select 元素,并且显示了其上下文提示。

分隔符装饰

<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 中移除了贴靠点,因为 Safari 难以实现水平滚动和贴靠效果组合。

媒体查询

针对较小视口的一项细微调整是隐藏“Home”标签,只留下图标:

@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 has changed 和 Eager <select> 更改事件触发预防。

由于使用了 <select> 元素,因此需要防止紧急事件。在 Windows Edge 上,可能还有其他浏览器,当用户使用键盘浏览选项时,会触发 select 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 上构建应用的所有方法。 创建一个演示,在 Twitter 微博中发送链接,然后我会将其添加到下面的“社区混剪”部分!

社区混剪作品