为网站构建主导航栏

本教程介绍了如何构建无障碍的网站主导航栏。您将了解语义 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);
}
查看在 CodePen 上完成的第 1 步:基本 HTML 和 CSS

这对大多数用户都很有效,无论他们通过何种方式访问网站。用户可以使用鼠标、键盘、触摸设备或屏幕阅读器访问导航栏,但仍有改进空间。您可以通过添加其他功能和信息来扩展此基本模式,从而提升用户体验。

您可以采取以下措施:

  • 突出显示当前页面。
  • 向屏幕阅读器用户读出项的数量。
  • 添加地图注点,并允许屏幕阅读器用户使用快捷方式直接访问导航栏。
  • 在狭窄的视口中隐藏导航栏。
  • 改进了焦点样式。

突出显示当前网页

如需突出显示当前页面,您可以向相应的链接添加类。

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

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

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 中的语音朗读功能的一个值得注意的细节是,设置 list-style: none 后,您将失去所有这些优势。这是设计所致。WebKit 团队决定在列表看起来不像列表时移除列表语义。这不一定是问题,具体取决于导航栏的复杂程度。一方面,导航功能仍然可用,并且仅会影响 Safari 中的 VoiceOver。搭配 Chrome 或 Firefox 使用时,VoiceOver 仍会读出内容数量,其他屏幕阅读器(例如 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 使用旋转图标列出页面上的所有地标。

四个地标的列表:横幅、导航栏、主要内容、内容信息。
VoiceOver 中的旋转图标,用于列出网页上的所有地标。

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

如果您在单个网页上同时使用了网站级导航栏、网页本地导航栏和分页功能,则可能还需要 3 个 <nav> 元素。这很好,但现在有三个导航地标,它们在语义上看起来都一样。除非您非常了解网页的结构,否则很难将它们区分开来。

一张图片,显示了三个地标,都标有“导航”字样。
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>

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

地标
语音提示列出了地标“横幅”“主导航栏”“主体内容”“页面导航”“选择页面导航”和“内容信息”。
查看第 4 步:在 CodePen 上添加地标

在窄视口上隐藏导航

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

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

添加三线状按钮

由于您遵循的是渐进增强原则,因此需要确保即使在停用 JavaScript 的情况下,导航栏也能正常运行且有意义。
导航栏首先需要一个三线状按钮。您需要在模板元素中使用 HTML 创建它,在 JavaScript 中克隆它,然后将其添加到导航栏中。

显示三线状菜单按钮的页面。
结果:在狭窄的视口中,导航栏会显示汉堡型三线按钮,而不是链接。
<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;
}

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

显示导航列表和汉堡式按钮的页面。
汉堡型三线按钮和列表都位于视口的顶部角落。

该列表显然需要一些 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 步:隐藏列表

为列表添加动画效果

如果您想知道为什么应使用 visibility: hidden; 而非 display: none;,原因在于您可以为可见性添加动画效果。它只有两个状态: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。
  • Edge:86。
  • Firefox:85.
  • Safari:15.4.

来源

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

您可以通过多种方式突出显示焦点所在的项。建议使用 outline 属性,因为它不会破坏布局(border 可能会出现这种情况),并且与 Windows 上的高对比度模式配合使用效果良好。效果不佳的房源是 background-colorbox-shadow,因为它们在使用自定义对比度设置时可能根本不会显示。

深色背景的网站,焦点以紫色突出显示。
查看第 8 步:改进 CodePen 上的焦点样式

恭喜!您已构建一个逐步增强、富含语义、易于访问且适合移动设备的主导航栏。

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

  • 您可以考虑在导航栏中捕获焦点,或者在窄视口中将页面的其余部分设为不活跃
  • 您可以在页面顶部添加跳转链接,以允许键盘用户跳过导航。

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

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

<nav> 元素具有隐式 ARIA 导航角色,足以表明该元素是导航元素,但您经常会看到网站还使用 menu、menubar 和 menuitem。由于我们有时会交替使用这两个术语,因此认为将它们结合起来以改善屏幕阅读器用户的体验可能是有意义的。在了解通常情况下情况并非如此之前,我们先来看看这些角色的官方定义。

导航角色

一组导航元素(通常是链接),用于导航文档或相关文档。

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),其中包括:

  • 使用 EnterSpace 键选择菜单项。
  • 使用上下左右箭头键在各项之间切换。
  • HomeEnd 键,分别用于将焦点移至第一个或最后一个项目。
  • 按 a-z 键可将焦点移到下一个带有以所输入字符开头的标签的菜单项。
  • Esc 键可关闭菜单。

如果屏幕阅读器检测到菜单,软件可能会自动更改浏览模式,以便使用前面提到的快捷键。不熟悉屏幕阅读器的用户可能无法使用该菜单,因为他们不知道这些快捷键或如何使用它们。

键盘用户可能希望使用 ShiftShift + Tab 键,这也是如此。

创建菜单和菜单栏时,需要考虑很多因素,首先要考虑是否适合使用它们。在构建典型的网站时,您只需使用包含列表和链接的 nav 元素即可。这也包括单页应用 (SPA) 或 Web 应用。底层堆栈无关紧要。除非您要构建的应用非常接近桌面应用,否则请避免使用菜单角色。

其他资源

主打图片:Mick Haupt