为网站构建主导航栏

本教程介绍了如何构建无障碍的网站主导航栏。您将了解语义 HTML、可访问性以及使用 ARIA 属性有时弊大于利的知识。

Manuel Matuzović
Manuel Matuzović

在样式、功能以及底层标记和语义信息方面,构建网站主导航栏的方法有很多种。如果实现方式过于简约,虽然它适合大多数人,但用户体验 (UX) 可能并不理想。 如果过度设计,则可能导致用户感到困惑,甚至妨碍他们访问。

对于大多数网站而言,您希望构建的网站既不简单也不复杂。

逐层构建

在本教程中,您将从基本设置开始,然后逐层添加地图项,直到提供的信息、样式和功能足以满足大多数用户的需求。为此,您需要利用渐进式增强原则,即从最基础且最可靠的解决方案开始,逐步添加功能层。如果某个层由于某种原因无法运行,导航仍会正常发挥作用,因为它可以正常回退到底层层。

基本结构

对于基本导航,您需要两项内容:<a> 元素和几行 CSS,以改进链接的默认样式和布局。

<a href="/home">Home</a>
<a href="/about-us">About us</a>
<a href="/pricing">Pricing</a>
<a href="/contact">Contact</a>
/* Define variables for your colors */
:root {
  --color-shades-dark: rgb(25, 25, 25);
}

/* Use the alternative box model
Details: <https://web.dev/learn/css/box-model/> */
*{
  box-sizing: border-box;
}

/* Basic font styling */
body {
  font-family: Segoe UI, system-ui, -apple-system, sans-serif;
  font-size: 1.6rem;
}

/* Link styling */
a {
  --text-color: var(--color-shades-dark);
  border-block-end: 3px solid var(--border-color, transparent);
  color: var(--text-color);
  display: inline-block;
  margin-block-end: 0.5rem; /* See note at the bottom of this chapter */
  margin-inline-end: 0.5rem;
  padding: 0.1rem;
  text-decoration: none;
}

/* Change the border-color on :hover and :focus */
a:where(:hover, :focus) {
  --border-color: var(--text-color);
}
查看第 1 步:基本 HTML 和 CSS(在 CodePen 中)。

这对大多数用户都很有效,无论他们通过何种方式访问网站。您可以使用鼠标、键盘、触摸设备或屏幕阅读器进行导航,但还有改进的空间。您可以在此基本模式中添加更多功能和信息,以改善体验。

您可以采取以下措施:

  • 突出显示当前页面。
  • 向屏幕阅读器用户读出项目数量。
  • 添加位置标记,让屏幕阅读器用户可以直接使用快捷方式访问导航。
  • 在窄视口上隐藏导航。
  • 改进焦点样式。

突出显示当前网页

如要突出显示当前页面,您可以在相应链接中添加课程。

<a href="/about-us" class="active-page">About us</a>

这种方法存在的问题是,它只能直观地传达哪个链接处于活动状态的信息。盲人屏幕阅读器用户无法区分当前打开的页面和其他页面。幸运的是,无障碍丰富互联网应用 (ARIA) 标准也提供了一种从语义上传达此类信息的方式。使用 aria-current=&quot;page&quot; 属性和值,而不是类。

<ph type="x-smartling-placeholder">
</ph> aria-current(状态)用于指明代表某个容器或一组相关元素中的当前项的元素。 用于指示一组分页链接中的链接的页面令牌,其中链接的样式已可表示当前显示的页面。 [无障碍丰富互联网应用 (WAI-ARIA) 1.1](https://www.w3.org/TR/wai-aria/#aria-current)

有了这个额外的属性,屏幕阅读器现在可以读出“当前页面、链接、关于我们”之类的内容而不只是“链接 - 关于我们”。

<a href="/about-us" aria-current="page" class="active-page">About us</a>

一种很方便的副作用是,您可以使用该属性在 CSS 中选择有效链接,使 active-page 类过时。

<a href="/home">Home</a>
<a href="/about-us" aria-current="page">About us</a>
<a href="/pricing">Pricing</a>
<a href="/contact">Contact</a>
/* Change border-color and color for the active page */
[aria-current="page"] {
  --border-color: var(--color-highlight);
  --text-color: var(--color-highlight);
}
查看第 2 步:在 CodePen 上突出显示活动页面

读出商品数量

通过查看导航,视力正常的用户会分辨其中只包含四个链接。盲人屏幕阅读器用户无法快速获得这些信息。他们可能需要逐个浏览所有链接。如果列表像此示例中那样很短,那么这可能就不会成为问题,但如果包含 40 个链接,此任务可能会很繁琐。如果屏幕阅读器用户预先知道导航中包含大量链接,他们可能会决定使用其他更高效的导航方式,例如网站搜索。
提前传达项目数量的一种不错方法是将每个链接封装在一个列表项 (<li>) 中,而列表项嵌套在无序列表 (<ul>) 中。

<ul>
  <li>
     <a href="/home">Home</a>
  </li>
  <li>
    <a href="/about-us" aria-current="page">About us</a>
  </li>
  <li>
    <a href="/pricing">Pricing</a>
  </li>
  <li>
    <a href="/contact">Contact</a>
  </li>
</ul>

当屏幕阅读器用户找到列表时,他们的软件会读出“列表,4 项”之类的内容。

以下演示了 Windows 上 NVDA 屏幕阅读器的导航方式。

现在,您必须调整样式,使其与之前一样。

/* Remove the default list styling and create a flexible layout for the list */
ul {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Basic link styling */
a {
  --text-color: var(--color-shades-dark);

  border-block-end: 3px solid var(--border-color, transparent);
  color: var(--text-color);
  padding: 0.1rem;
  text-decoration: none;
}

对于屏幕阅读器用户,使用列表有诸多好处:

  • 他们可以在与项目交互之前获取项目总数。
  • 他们可能会使用快捷方式在列表项之间跳转。
  • 他们可能会使用快捷方式在列表间跳转。
  • 屏幕阅读器可能会读出当前项的索引(例如,“列表项,共四项”)。

此外,如果网页不是用 CSS 呈现的,该列表会将链接显示为连贯的项目组,而不是一堆链接。

关于 Safari 中 VoiceOver 的一个值得注意的细节是,设置 list-style: none 后,您将失去所有这些优势。这是设计所致。WebKit 团队决定当列表看起来不像列表时移除列表语义。这不一定是个问题,具体取决于导航的复杂程度。一方面,导航仍然可用,并且仅影响 Safari 中的 VoiceOver。将“旁白”与 Chrome 或 Firefox 搭配使用时,仍会读出项目数量以及其他屏幕阅读器(如 NVDA)的数量。另一方面,语义信息在某些情况下可能非常有用。为此,您应邀请真实屏幕阅读器用户测试导航功能,并收集他们的反馈。如果您希望 Safari 中的 VoiceOver 功能与其他屏幕阅读器一样,可以通过在 <ul> 上明确设置 ARIA 列表角色来解决此问题。这会将行为还原为您移除列表样式之前的状态。从视觉上看,该列表看起来仍然没什么变化。

<ul role="list">
  <li>
     <a href="/home">Home</a>
  </li>
  ...
</ul>
查看第 3 步:公布 CodePen 上的项目数量

添加地标

不费吹灰之力,您已经为屏幕阅读器用户做出了很大的改进,但您还可以做一件事。从语义上来讲,导航元素仍然只是一个链接列表,用户很难分辨这个列表就是您网站的主要导航元素。您可以将 <ul> 封装在 <nav> 元素中,从而将此普通列表转换为导航列表。

使用 <nav> 元素有几个优点。值得注意的是,当用户与屏幕阅读器交互时,屏幕阅读器会读出诸如“导航”之类的内容,并在页面中添加一个地标。地标是网页上的特殊区域(例如 <header><footer><main>),屏幕阅读器可以跳转到这些区域。在网页上添加地标非常实用,因为屏幕阅读器用户可以直接访问网页上的重要区域,而无需与网页的其余部分互动。例如,您可以在 NVDA 中按 D 键,从地标间跳转。在旁白中,您可以按 VO + U,使用转子列出页面上的所有地标。

<ph type="x-smartling-placeholder">
</ph> 四个位置标记的列表:横幅、导航、主要、内容信息。
VoiceOver 中的旋转器,可列出页面上的所有地标。

在此列表中,您会看到 4 个地标:banner<header> 元素)、navigation<nav>)、main<main> 元素)和 content information(内容信息)为 <footer>。该列表应该不会过长,您实际上只需要将界面中的关键部分(如网站搜索、本地导航或分页)标记为地标。

如果您设置网站级导航、网页本地导航和单个网页分页,则可能还拥有 3 个 <nav> 元素。没关系,但现在有三个导航地标,从语义上讲,它们都看起来一样。除非您非常了解网页的结构,否则很难区分它们。

<ph type="x-smartling-placeholder">
</ph> 此图片显示了三个全部显示“导航”的地标。
VoiceOver 中的转子列出了三个无标签导航标记。

为了让它们具有辨识度,您应使用 aria-labelledbyaria-label 对其进行标记。

<nav aria-label="Main">
    <ul>
      <li>
         <a href="/home">Home</a>
      </li>
      ...
  </ul>
</nav>
...
<nav aria-label="Select page">
    <ul>
      <li>
         <a href="/page-1">1</a>
      </li>
      ...
    </ul>
</nav>

如果您选择的标签已存在于网页的某处,则可以改用 aria-labelledby,并使用 id 属性引用现有标签。

<nav aria-labelledby="pagination_heading">
  <h2 id="pagination_heading">Select a page</h2>
  <ul>
    <li>
       <a href="/page-1">1</a>
    </li>
    ...
  </ul>
</nav>

使用简洁的标签就足够了,不要太冗长。省略“导航”等表达式或“菜单”因为屏幕阅读器已经为用户提供了这些信息。

<ph type="x-smartling-placeholder">
</ph> 地标
通过 VoiceOver 列出“横幅广告”“主导航”“主”“页面导航”“选择页面导航”和“内容信息”
查看第 4 步:在 CodePen 上添加地标

在窄视口上隐藏导航

就我个人而言,我并不喜欢在狭窄的视口中隐藏主导航栏,但是如果链接列表过长,就无法解决问题。在这种情况下,用户看到的不是列表,而是标记为“菜单”的按钮汉堡图标或两者的组合。点击该按钮可显示和隐藏列表。如果您了解基本的 JavaScript 和 CSS,这是一项可以处理的任务,但在用户体验和无障碍功能方面,您必须注意一些事项。

  • 您必须以可访问的方式隐藏列表。
  • 导航方式必须可通过键盘进行访问。
  • 导航栏必须表明其是否可见。

添加汉堡按钮

由于您遵循的是渐进式增强原则,因此您需要确保导航功能在关闭 JavaScript 时仍能正常运行且可用。
导航功能首先需要一个汉堡按钮。您可以在模板元素中使用 HTML 创建它,在 JavaScript 中克隆它,然后将其添加到导航中。

<ph type="x-smartling-placeholder">
</ph> 显示汉堡按钮的页面。
结果:在窄视口上,导航会显示汉堡式按钮,而不是显示链接。
<nav id="mainnav">
  ...
</nav>

<template id="burger-template">
  <button type="button" aria-expanded="false" aria-label="Menu" aria-controls="mainnav">
    <svg width="24" height="24" aria-hidden="true">
      <path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z">
    </svg>
  </button>
</template>
  1. aria-expanded 属性可告知屏幕阅读器软件按钮控件的元素是否已展开。
  2. aria-label 为按钮提供一个所谓的无障碍名称,即汉堡图标的替代文本。
  3. 您使用 aria-hidden 对辅助技术隐藏了 <svg>,因为它已包含 aria-label 提供的文本标签。
  4. aria-controls 用于告知支持属性(例如 JAWS)的辅助技术,即按钮所控制的元素。
const nav = document.querySelector('#mainnav')
const list = nav.querySelector('ul');
const burgerClone = document.querySelector('#burger-template').content.cloneNode(true);
const button = burgerClone.querySelector('button');

// Toggle aria-expanded attribute
button.addEventListener('click', e => {
  // aria-expanded="true" signals that the menu is currently open
  const isOpen = button.getAttribute('aria-expanded') === "true"
  button.setAttribute('aria-expanded', !isOpen);
});

// Hide list on keydown Escape
nav.addEventListener('keyup', e => {
  if (e.code === 'Escape') {
    button.setAttribute('aria-expanded', false);
  }
});

// Add the button to the page
nav.insertBefore(burgerClone, list);
  1. 这样便于用户随时根据需要关闭导航,例如按 Esc 键即可。
  2. 请务必使用 insertBefore,而不是 appendChild,因为按钮应该是导航中的第一个元素。如果键盘或屏幕阅读器用户在点击该按钮后按下 Tab,他们希望聚焦于列表中的第一项。如果按钮位于列表之后,情况就会不是这样。

接下来,重置按钮的默认样式,并确保该按钮仅在较窄的视口上可见。

@media (min-width: 48em) {
  nav {
    --nav-button-display: none;
  }
}

/* Reset button styling */
button {
  all: unset;
  display: var(--nav-button-display, flex);
}
查看第 5 步:在 CodePen 上添加汉堡式按钮

隐藏列表

在隐藏列表之前,请设置导航栏和列表的位置并设置其样式,以便布局针对窄视口进行了优化,但在较大屏幕上仍能正常显示。
首先,从页面的自然流动中移除 <nav>,并将其放置在视口的顶角。

@media (min-width: 48em) {
  nav {
    --nav-button-display: none;
    --nav-position: static;
  }
}

nav {
  position: var(--nav-position, fixed);
  inset-block-start: 1rem;
  inset-inline-end: 1rem;
}

接下来,通过添加新的自定义属性 (—-nav-list-layout) 来更改窄视口上的布局。默认情况下,该布局是列式布局,在较大的屏幕上会切换为行式布局。

@media (min-width: 48em) {
  nav {
    --nav-button-display: none;
    --nav-position: static;
  }

  ul {
    --nav-list-layout: row;
  }
}

ul {
  display: flex;
  flex-direction: var(--nav-list-layout, column);
  flex-wrap: wrap;
  gap: 1rem;
  list-style: none;
  margin: 0;
  padding: 0;
}

在窄视口上,您的导航栏应如下所示。

<ph type="x-smartling-placeholder">
</ph> 显示导航列表和汉堡式按钮的页面。
汉堡形按钮和列表均位于视口的顶角。

该列表显然需要一些 CSS。我们将它移到顶端角落,使其垂直填满整个屏幕,然后应用 background-colorbox-shadow

@media (min-width: 48em) {
  nav {
    --nav-button-display: none;
    --nav-position: static;
  }
  
  ul {
    --nav-list-layout: row;
    --nav-list-position: static;
    --nav-list-padding: 0;
    --nav-list-height: auto;
    --nav-list-width: 100%;
    --nav-list-shadow: none;
  }
}

ul {
  background: rgb(255, 255, 255);
  box-shadow: var(--nav-list-shadow, -5px 0 11px 0 rgb(0 0 0 / 0.2));
  display: flex;
  flex-direction: var(--nav-list-layout, column);
  flex-wrap: wrap;
  gap: 1rem;
  height: var(--nav-list-height, 100vh);
  list-style: none;
  margin: 0;
  padding: var(--nav-list-padding, 2rem);
  position: var(--nav-list-position, fixed);
  inset-block-start: 0; /* Logical property. Equivalent to top: 0; */
  inset-inline-end: 0; /* Logical property. Equivalent to right: 0; */
  width: var(--nav-list-width, min(22rem, 100vw));
}

button {
  all: unset;
  display: var(--nav-button-display, flex);
  position: relative;
  z-index: 1;
}

在窄视口上,该列表应如下所示,更像是边栏,而不是简单列表。

系统会打开导航列表。

最后,隐藏列表,仅在用户点击按钮一次时显示,在用户点击按钮时隐藏。请务必仅隐藏列表而非整个导航,因为隐藏导航也意味着隐藏重要的地标。

之前,您为按钮添加了一个点击事件,用于切换 aria-expanded 属性的值。您可以使用该信息作为在 CSS 中显示和隐藏列表的条件。

@media (min-width: 48em) {
  ul {
    --nav-list-visibility: visible;
  }
}

ul {
  visibility: var(--nav-list-visibility, visible);
}

/* Hide the list on narrow viewports, if it comes after an element with
   aria-expanded set to "false". */
[aria-expanded="false"] + ul {
  visibility: var(--nav-list-visibility, hidden);
}

请务必使用 visibility: hiddendisplay: none 等属性声明(而不是 opacity: 0translateX(100%))来隐藏列表。这些属性可确保导航栏在隐藏时无法聚焦。使用 opacitytranslate 会直观地移除内容,使链接不可见,但仍可通过键盘访问,这会使用户感到困惑和沮丧。使用 visibilitydisplay 会在视觉上将其隐藏起来,使其无法访问,从而对所有用户隐藏它。

查看第 6 步:隐藏列表

为列表添加动画效果

如果您想知道为什么在 display: none; 上使用 visibility: hidden;,这是因为您可以为可见性添加动画效果。它只有 hiddenvisible 两种状态,但您可以将其与 transformopacity 等其他属性结合使用,以创建滑动或淡入效果。这不适用于 display: none,因为 display 属性不可添加动画效果。

以下 CSS 转换 opacity 以创建淡入和淡出效果。

ul {
  transition: opacity 0.6s linear, visibility 0.3s linear;
  visibility: var(--nav-list-visibility, visible);
}

[aria-expanded="false"] + ul {
  opacity: 0;
  visibility: var(--nav-list-visibility, hidden);
}

如果您想改为以动画方式呈现动作,则应考虑将 transition 属性封装在 prefers-reduced-motion 媒体查询中,因为对于某些用户,动画可能会引起恶心、头晕和头痛

ul {
  visibility: var(--nav-list-visibility, visible);
}

@media (prefers-reduced-motion: no-preference) {
  ul {
    transition: transform 0.6s cubic-bezier(.68,-0.55,.27,1.55), visibility 0.3s linear;
  }
}

[aria-expanded="false"] + ul {
  transform: var(--nav-list-transform, translateX(100%));
  visibility: var(--nav-list-visibility, hidden);
}

这样可以确保只有不喜欢减少动态的人才能看到动画。

查看第 7 步:在 CodePen 上为列表添加动画效果

改进焦点样式

键盘用户依赖元素的焦点样式来确定页面上的方向和导航方式。默认焦点样式要优于无焦点样式(如果您设置了 outline: none,就会发生这种情况),但具有更清晰的自定义焦点样式可提升用户体验。

以下是 Chrome 103 中链接的默认焦点样式的外观。

Chrome 103 中聚焦的链接周围有一个 2 像素的蓝色轮廓。

您可以通过采用自己的颜色提供自己的样式来提高这一点。通过使用 :focus-visible 而不是 :focus,您可以让浏览器决定何时显示焦点样式。所有用户(包括鼠标用户、键盘用户和触摸用户)均可看到:focus样式,无论他们是否需要这些样式。对于 :focus-visible,浏览器会使用内部启发法来决定是仅向键盘用户显示这些内容,还是向所有人显示。

/* Remove the default :focus outline */
*:focus {
  outline: none;
}

/* Show a custom outline on :focus-visible */
*:focus-visible {
  outline: 2px solid var(--color-shades-dark);
  outline-offset: 4px;
}

:focus-visible 的浏览器支持情况

浏览器支持

  • Chrome:86。 <ph type="x-smartling-placeholder">
  • Edge:86。 <ph type="x-smartling-placeholder">
  • Firefox:85。 <ph type="x-smartling-placeholder">
  • Safari:15.4. <ph type="x-smartling-placeholder">

来源

清晰可见的深色 2 像素轮廓,内含间距。

您可以通过不同的方式突出显示获得焦点的项。建议使用 outline 属性,因为它不会破坏布局(使用 border 时可能会发生这种情况),并且它在 Windows 上的高对比度模式中效果很好。效果不佳的属性是 background-colorbox-shadow,因为它们可能根本无法在采用自定义对比度设置时显示。

深色背景和紫色高亮显示的网站。
查看第 8 步:改进 CodePen 上的焦点样式

恭喜!您已经构建了一个渐进式增强、语义丰富、易于访问且适合移动设备的主导航栏。

总有一些地方可以改进,例如:

  • 您可以考虑将焦点置于导航栏中,或使网页的其余部分在较窄的视口中"惯用"。
  • 您可以在页面顶部添加跳过链接,以允许键盘用户跳过导航。

还记得本文最初是如何开始的,目的就是“既不简单也不复杂”,这就是我们现在所处的位置。不过,您可以过度设计导航。

导航栏和菜单有明显的区别。导航组件是用于浏览相关文档的链接集合。菜单是要在文档中执行的操作的集合。有时,这些任务会重叠。您的导航栏可能还包含一个可执行操作(如打开模态窗口)的按钮,也可能有一个菜单,在该菜单中,一项操作会导航到另一个页面(如帮助页面)。在这种情况下,请不要混搭 ARIA 角色,而应确定组件的主要用途并相应地选择标记和角色。

<nav> 元素具有隐式 ARIA 导航角色,这足以表明该元素是导航栏,但网站经常还会使用菜单、菜单栏和菜单项。由于我们有时会互换使用这些术语,因此认为将它们结合起来可以改善屏幕阅读器用户的体验或许是合理的。在我们了解为什么实际情况并非如此之前,我们先了解一下这些角色的官方定义。

导航角色

用于导航文档或相关文档的导航元素(通常是链接)的集合。

navigation(角色)WAI-ARIA 1.1

菜单角色

菜单通常是用户可以调用的常见操作或功能的列表。如果菜单项列表的呈现方式与桌面应用中的菜单类似,则可以使用菜单角色

菜单(角色)WAI-ARIA 1.1

菜单栏角色

菜单的显示画面,菜单通常保持可见状态且通常水平显示。 菜单栏角色用于创建与 Windows、Mac 和 Gnome 桌面应用程序类似的菜单栏。菜单栏用于创建一组一致的常用命令。开发者应该确保菜单栏交互类似于桌面图形界面中的典型菜单栏交互。

菜单栏(角色)WAI-ARIA 1.1

Menuitem 角色

菜单菜单栏包含的一组选项中的一个选项。

menuitem(角色)WAI-ARIA 1.1

此处的规范非常明确,使用导航来浏览文档或相关文档,以及使用菜单浏览一系列操作或功能(类似于桌面应用中的菜单)。如果您没有构建下一个 Google 文档,则可能不需要主导航栏的任何菜单角色。

菜单在什么情况下合适?

菜单项的主要用途不是导航,而是用来执行操作。假设您有一个数据列表或表格,用户可以对列表中的每一项执行某些操作。您可以在每行中添加一个按钮,并在用户点击该按钮时显示相应操作。

<ul>
  <li>
    Product 1

    <button aria-expanded="false" aria-controls="options1">Edit</button>

    <div role="menu" id="options1">
      <button role="menuitem">
        Duplicate
      </button>
      <button role="menuitem">
        Delete
      </button>
      <button role="menuitem">
        Disable
      </button>
    </div>
  </li>
  <li>
    Product 2
    ...
  </li>
</ul>

使用菜单角色的影响

明智地使用这些菜单角色非常重要,因为会有很多错误。

菜单需要特定的 DOM 结构。“menuitem”必须是“menu”的直接子项。以下代码可能会破坏语义行为:

 <!-- Wrong, don't do this -->
<ul role="menu">
  <li>
    <a href="#" role="menuitem">Item 1</a>
  </li>
</ul>

有经验的用户希望将某些键盘快捷键用于菜单和菜单栏。根据 ARIA 制作实践指南 (APG),其中包括:

  • Enter 键和空格键即可选择菜单项。
  • 按各个方向的箭头键可在各项内容之间导航。
  • HomeEnd 键,可分别将焦点移到第一项或最后一项。
  • 按 a-z 键可将焦点移到下一个带有以所输入字符开头的标签的菜单项。
  • Esc 键可关闭菜单。

如果屏幕阅读器检测到菜单,该软件可能会自动更改浏览模式,让用户能够使用上述快捷键。没经验的屏幕阅读器用户可能无法使用菜单,因为他们不了解这些快捷键或如何使用它们。

对于可能希望能够使用 ShiftShift + Tab 的键盘用户来说,也是如此。

在创建菜单和菜单栏时,有许多方面需要考虑,即能否从一开始就使用它们。构建典型网站时,只需使用带有列表和链接的 nav 元素即可。这也包括单页应用 (SPA) 或 Web 应用。底层堆栈无关紧要。除非您要构建与桌面应用非常相似的元素,否则请避免使用菜单角色。

其他资源

主打图片:Mick Haupt