为网站构建主导航栏

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

曼努埃尔·马图佐维奇
Manuel Matuzoviique

根据样式、功能以及底层标记和语义信息,可以采用多种不同的方法构建网站的主导航。如果实现过于简约,则适合大多数人,但用户体验 (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="page" 属性和值,而不是类。

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 个链接,此任务可能会很繁琐。如果屏幕阅读器用户事先知道导航中包含大量链接,他们可能会决定采用另一种更高效的导航方式,例如网站搜索。
预先传达列表项数量的一个好方法是,将每个链接封装在一个无序列表 (<ul>) 中的列表项 (<li>) 中。

<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 中“旁白”的值得注意的一点是,设置 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> 元素;内容信息<footer>。此列表应该不会过长,您实际上只需要将界面的关键部分标记为地标,例如网站搜索、本地导航或分页。

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

图片中显示了三个都带有“navigation”字样的地标。
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>

简洁的标签就足够了,不要过于冗长。请省略“navigation”或“menu”等表达式,因为屏幕阅读器已为用户提供了这些信息。

地标
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-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 的支持

浏览器支持

  • 86
  • 86
  • 85
  • 15.4

来源

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

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

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

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

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

  • 您可以考虑将焦点限制在导航内,或者在窄视口上使网页的其余部分“不动”
  • 您可以在页面顶部添加“跳过”链接,以允许键盘用户跳过导航。

如果您还记得本文的开头部分,而且我们的目标让解决方案“既不会太简单也不要太复杂”,那正是我们现在的工作。不过,有可能对导航进行过度工程。

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

<nav> 元素具有隐式的导航 ARIA 角色,这足以表明该元素为导航,但经常您会看到网站还使用菜单、菜单栏和菜单项。由于我们有时会互换使用这些术语,所以不妨考虑将它们组合起来以改善屏幕阅读器用户的体验。在了解为什么通常并非如此,我们先来看一下这些角色的官方定义。

导航角色

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

导航(角色)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 键和空格键可选择菜单项。
  • 按全部方向键即可浏览各项内容。
  • Home 键和 End 键可分别将焦点移到第一项或最后一项。
  • 按 a-z 键可将焦点移至下一个带有以输入字符开头的菜单项。
  • Esc 关闭菜单。

如果屏幕阅读器检测到菜单,该软件可能会自动更改浏览模式,允许使用之前提到的快捷键。经验不足的屏幕阅读器用户可能无法使用菜单,因为他们不了解这些快捷键或其使用方法。

这同样适用于可能会希望使用 ShiftShift + Tab 的键盘用户。

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

其他资源

主打图片:Mick Haupt