构建面包屑导航组件

本课程简要介绍了如何构建响应式且易于访问的面包屑导航组件,以便用户浏览您的网站。

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

演示

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

概览

面包屑导航组件会显示用户在网站层次结构中的位置。这个名称源自《汉泽尔和格雷特》的故事:汉泽尔和格雷特在黑暗的树林中行走时,在后面撒下面包屑,然后沿着面包屑的方向回到了家。

本文中的面包屑导航不是标准面包屑导航,而是类似于面包屑导航。它们通过使用 <select> 将同级页面直接放入导航栏中,从而提供额外的功能,实现多层级访问。

背景用户体验

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

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

信息架构

我发现,从集合和项的角度考虑会很有帮助。

集合

集合是可供选择的选项数组。在本帖子面包屑导航原型版的首页中,集合包括第一人称射击游戏、角色扮演游戏、格斗游戏、地下城探索游戏、体育游戏和益智游戏。

项目

视频游戏是一种项,特定合集也可以是项,前提是它代表另一个合集。例如,“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 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 上看到它占据醒目的位置。一个属性可为许多用户提供按钮上下文。

屏幕截图:悬停在不可见的选择元素上,并显示其上下文提示。

分隔符装饰

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

分隔符是可选的,只添加一个分隔符也非常有效(请参阅上方视频中的第三个示例)。然后,我为每个 aria-hidden="true" 都设置了 ARIA 无障碍功能,因为它们是装饰性元素,不需要屏幕阅读器读出。

gap 属性(下文将介绍)可让您轻松设置这些元素之间的间距。

样式

由于颜色使用的是系统颜色,因此样式大多是间距和堆叠!

布局方向和流程

使用 Flexbox 叠加功能显示面包屑导航栏对齐情况的 DevTools。

主要导航元素 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> 元素堆叠在一起。

网格 DevTools 显示在按钮上方,其中行和列都命名为“堆叠”。

.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 事件。因此,我将其称为“提前”,因为用户只是伪选中了选项(例如悬停或聚焦),但尚未使用 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 上构建的所有方式。 制作一个演示版,在推特上向我发送链接,我会将其添加到下方的社区混剪部分!

社区混剪作品