为网站构建主导航栏

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

Manuel Matuzović
Manuel Matuzović

在样式、功能以及底层标记和语义信息方面,构建网站主导航的方式有很多种。如果实现过于简单,虽然对大多数人都适用,但用户体验 (UX) 可能不太好。 如果设计过于复杂,可能会让用户感到困惑,甚至导致他们根本无法访问。

对于大多数网站,您需要构建一个既不太简单也不太复杂的网站。

逐层构建

在本教程中,您将从基本设置开始,逐层添加功能,直到提供足够的信息、样式和功能,让大多数用户满意。为此,您需要遵循渐进增强原则,即从最基本、最强大的解决方案开始,逐步添加功能层。如果某一层因某种原因无法正常工作,导航仍可正常运行,因为它会平稳地回退到基础层。

基本结构

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

  • HTML

    <a href="/home">Home</a>
    <a href="/about-us">About us</a>
    <a href="/pricing">Pricing</a>
    <a href="/contact">Contact</a>
    
  • CSS

    /* 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="page" 属性和值,而不是类。

aria-current(状态)表示容器或一组相关元素中代表当前项的元素。 一个页面令牌,用于指示一组分页链接中的某个链接,该链接在视觉上采用特定样式,以表示当前显示的页面。 [Accessible Rich Internet Applications (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;
}

使用列表可以为屏幕阅读器用户带来许多优势:

  • 他们可以在与内容互动之前获取内容总数。
  • 他们可能会使用快捷键在列表项之间跳转。
  • 他们可能会使用快捷键在列表之间跳转。
  • 屏幕阅读器可能会读出当前项的索引(例如,“列表项,第 2 项,共 4 项”)。

此外,如果页面在没有 CSS 的情况下呈现,列表会将链接显示为一组连贯的项,而不仅仅是一堆链接。

关于 Safari 中的 VoiceOver,一个值得注意的细节是,如果您设置 list-style: none,就会失去所有这些优势。这是设计所致。WebKit 团队决定在列表看起来不像列表时移除列表语义。这是否会成为问题,取决于导航的复杂程度。一方面,导航功能仍然可用,并且仅影响 Safari 中的 VoiceOver。VoiceOver 搭配 Chrome 或 Firefox 仍会播报内容数量,其他屏幕阅读器(例如 NVDA)也是如此。另一方面,语义信息在某些情况下可能非常有用。为了做出这一决定,您应该让实际的屏幕阅读器用户测试导航功能,并收集他们的反馈。如果您希望 Safari 中的 VoiceOver 像所有其他屏幕阅读器一样运行,则可以通过在 <ul> 上明确设置 ARIA 列表角色来解决此问题。这会将行为还原到移除列表样式之前的状态。从视觉上看,列表仍然保持原样。

<ul role="list">
  <li>
     <a href="/home">Home</a>
  </li>
  ...
</ul>
在 CodePen 上查看第 3 步:播报商品数量。

添加地标

您只需稍作努力,就能为屏幕阅读器用户带来极大改进,但您还可以做一件事。从语义上讲,导航仍然只是一个链接列表,很难看出这个特定列表是您网站的主导航。您可以将 <ul> 封装在 <nav> 元素中,从而将此普通列表转换为导航列表。

使用 <nav> 元素有以下几个优势。值得注意的是,当用户与屏幕阅读器互动时,屏幕阅读器会朗读“导航”之类的内容,并向页面添加地标。地标是网页上的特殊区域,例如 <header>、<footer> 或 <main>,屏幕阅读器可以跳转到这些区域。在网页上设置地标非常有用,因为这样一来,屏幕阅读器用户无需与网页的其他部分互动,即可直接访问网页上的重要区域。例如,在 NVDA 中,您可以按 D 键在各个位置标记之间跳转。在 VoiceOver 中,您可以按 VO + U,使用转子列出页面上的所有地标。

包含四个地标的列表:横幅、导航、主要、内容信息。
VoiceOver 中的转子,列出了页面上的所有地标。

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

如果您有网站级导航、网页的本地导航以及单个网页上的分页,则可能还会有 3 个 <nav> 元素。这没问题,但现在有三个导航地标,而且它们在语义上看起来都一样。除非您对页面结构非常了解,否则很难区分它们。

图片:显示三个地标,均标有“导航”。
VoiceOver 中的转子列出了三个未加标签的导航地标。

为了区分它们,您应该使用 aria-labelledby 或 aria-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>

标签应简明扼要,不要过于冗长。省略“导航”或“菜单”等表达方式,因为屏幕阅读器已向用户提供此类信息。

地标
VoiceOver 列出了地标“横幅”“主导航”“主”“网页导航”“选择网页导航”和“内容信息”。
查看第 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-color 和 box-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: hidden 或 display: none 等属性声明(而非 opacity: 0 或 translateX(100%))来隐藏列表。这些属性可确保在导航栏隐藏时,链接不可聚焦。使用 opacity 或 translate 会在视觉上移除内容,因此链接将不可见,但仍可使用键盘访问,这会让人感到困惑和沮丧。使用 visibility 或 display 会在视觉上隐藏该元素并使其无法访问,从而对所有用户隐藏该元素。

请参阅第 6 步:隐藏列表。

为列表添加动画效果

如果您想知道为什么要使用 visibility: hidden; 而不是 display: none;,原因在于您可以为可见性添加动画效果。它只有两种状态:hidden 和 visible,但您可以将其与 transform 或 opacity 等其他属性结合使用,以创建滑入或淡入效果。这不适用于 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 的浏览器支持

Browser Support

  • Chrome: 86.
  • Edge: 86.
  • Firefox: 85.
  • Safari: 15.4.

Source

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

当内容获得焦点时,有多种不同的突出显示方式。建议使用 outline 属性,因为它不会破坏布局(border 可能会破坏布局),并且可以很好地与 Windows 上的高对比度模式搭配使用。background-color 或 box-shadow 属性的效果不佳,因为它们可能根本不会在自定义对比度设置中显示。

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

恭喜!您已构建了一个渐进增强、语义丰富、无障碍且适合移动设备的主导航。

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

如果您还记得本文开头提到的目标,即解决方案应该“既不太简单,也不太复杂”,那么我们现在就处于这个阶段。不过,导航也可能会过度设计。

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

<nav> 元素具有隐式 ARIA 角色“navigation”,这足以传达该元素是导航,但您经常会看到网站也使用“menu”“menubar”和“menuitem”。由于我们有时会交替使用这些术语,因此认为将它们结合起来以提升屏幕阅读器用户的体验可能是有意义的。在了解为什么通常并非如此之前,我们先来看看这些角色的官方定义。

导航角色

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

navigation(角色)WAI-ARIA 1.1

菜单角色

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

menu(角色)WAI-ARIA 1.1

menubar 角色

通常保持可见且通常水平显示的菜单。 menubar 角色用于创建类似于 Windows、Mac 和 Gnome 桌面应用中的菜单栏。菜单栏用于创建一组一致的常用命令。作者应确保菜单栏互动与桌面图形界面中的典型菜单栏互动类似。

menubar(角色)WAI-ARIA 1.1

menuitem 角色

menu 或 menubar 中包含的一组选项中的一个选项。

menuitem(角色)WAI-ARIA 1.1

规范对此有非常明确的规定:使用 navigation 元素在文档或相关文档中导航,而 menu 元素仅用于列出操作或功能,类似于桌面应用中的菜单。如果您不是在构建下一个 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 和空格键:选择菜单项。
  • 使用各个方向的箭头键可在各项之间切换。
  • Home 键和 End 键,分别用于将焦点移至第一项或最后一项。
  • a-z:将焦点移至标签以所输入字符开头的下一个菜单项。
  • 按 Esc 键可关闭菜单。

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

对于键盘用户来说也是如此,他们可能希望能够使用 Shift 和 Shift + Tab。

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

其他资源