构建面包屑导航组件

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

在这篇博文中,我想分享有关如何构建面包屑组件的一些想法。 试用演示版

演示

如果您更喜欢视频,可以观看此帖子的 YouTube 版本:

概览

面包屑组件用于显示用户在网站层次结构中的位置。该名称源自童话故事《汉赛尔和格蕾特》,故事中的主人公在黑暗的森林中沿途丢弃面包屑,然后通过追踪面包屑找到了回家的路。

此帖子中的面包屑不是标准面包屑,而是类似面包屑。它们通过将同级网页直接放入带有 <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 角色“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>

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

只需定义一次,即可根据需要多次使用,同时最大限度地减少对网页性能的影响,并提供灵活的样式设置。请注意,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 上看到它位于显眼位置。一个属性可为许多用户提供按钮上下文。

屏幕截图:鼠标悬停在不可见的 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 还建立了一个具有一定间隙的水平垂直对齐布局,但专门针对其链接子级并指定样式 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> 元素堆叠在一起。

网格开发者工具显示在按钮上方,其中行和列均命名为“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;
  }}
}

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

  • 具有过滚动限制的横向滚动。
  • 横向滚动内边距。
  • 最后一个面包屑上的一个贴靠点。这意味着,在页面加载时,第一个面包屑会加载到视图中。
  • 从 Safari 中移除了贴靠点,因为 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> 元素,因此需要防止过早触发事件。在 Windows Edge(可能还有其他浏览器)中,当用户使用键盘浏览选项时,会触发 select changed 事件。这就是我将其称为“急切”的原因,因为用户只是伪选择了该选项(例如悬停或聚焦),但尚未通过 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> 元素的事件触发时决定是等待还是继续。

总结

现在您已经知道我是如何做到的,那么您会怎么做呢?🙂

让我们丰富方法,了解在网络上构建内容的所有方式。 制作演示视频,通过 Twitter 向我发送链接,我会将其添加到下方的社区混音部分!

社区混音作品