用 CSS 解决!下拉菜单

Avatar of Una Kravets
Una Kravets

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

CSS 变得越来越强大,并且随着 CSS 网格和自定义属性(也称为 CSS 变量)等功能的出现,我们看到了许多真正有创意的解决方案。其中一些解决方案不仅专注于使网络更美观,还专注于使网络更易于访问,并改善易于访问的样式体验。我非常赞同!

文章系列

  1. 为 SVG 背景着色
  2. 下拉菜单(本文)
  3. 基于给定元素数量的逻辑样式

我们在网络上看到的一种常见的 UI 模式是下拉菜单。它们用于分段显示相关信息,而不会让用户感到按钮、文本和选项过多。我们经常在网站的页眉或导航区域中看到它们。

A collage of screenshots showing different dropdown menu examples.
在 Google 中搜索“下拉菜单”会产生许多示例

让我们看看是否可以仅使用 CSS 创建其中一个菜单。我们将像这样在 nav 组件中创建一个链接列表

<nav role="navigation">
  <ul>
    <li><a href="#">One</a></li>
    <li><a href="#">Two</a></li>
    <li><a href="#">Three</a></li>
  </ul>
</nav>

现在,假设我们想要在第二个导航项目上创建一个子菜单下拉菜单。我们可以在那里做同样的事情,并在该列表项中包含一个链接列表

<nav role="navigation">
  <ul>
    <li><a href="#">One</a></li>
    <li><a href="#">Two</a>
      <ul class="dropdown">
        <li><a href="#">Sub-1</a></li>
        <li><a href="#">Sub-2</a></li>
        <li><a href="#">Sub-3</a></li>
      </ul>
    </li>
    <li><a href="#">Three</a></li>
  </ul>
</nav>

现在我们有了两级导航系统。为了在我们需要时隐藏和显示内容,我们需要应用一些 CSS。为了清楚地说明交互,以下示例已删除所有样式属性

li {
 display: block;
 transition-duration: 0.5s;
}

li:hover {
  cursor: pointer;
}

ul li ul {
  visibility: hidden;
  opacity: 0;
  position: absolute;
  transition: all 0.5s ease;
  margin-top: 1rem;
  left: 0;
  display: none;
}

ul li:hover > ul,
ul li ul:hover {
  visibility: visible;
  opacity: 1;
  display: block;
}

ul li ul li {
  clear: both;
  width: 100%;
}

现在,子菜单下拉菜单是隐藏的,但当我们将鼠标悬停在导航栏中与其对应的父级上时,它将显示并变为可见。通过为 ul li ul 设置样式,我们可以访问该子菜单,并且通过为 ul li ul li 设置样式,我们可以访问其中的各个列表项。

问题

这开始看起来像我们想要的样子,但我们离完成还有很远。Web 可访问性是产品开发的核心部分,现在是提出这一点的绝佳机会。添加 role="navigation" 是一个良好的开端,但为了使导航栏可访问,用户应该能够通过 Tab 键遍历它(并按合理的顺序聚焦到正确的项目),并且屏幕阅读器也应该能够准确地大声朗读正在聚焦的内容。

您可以将鼠标悬停在任何列表项上,并清楚地看到鼠标悬停的位置,但这对于 Tab 键导航并不适用。请尝试通过上面的示例进行 Tab 键导航。您会失去对焦点位置的视觉跟踪。当您在主菜单中切换到 **Two** 时,您会看到一个焦点指示器环,但是当您切换到下一个项目(其子菜单项之一)时,该焦点会消失。

An animated screenshot showing focus rings on menu items as they are tabbed.

现在,需要注意的是,理论上您正在聚焦于此其他项目,并且屏幕阅读器能够解析它,读取 **Sub-One**,但键盘用户将无法看到发生了什么,并且会失去跟踪。

发生这种情况的原因是,虽然我们正在设置父元素的悬停样式,但一旦我们将焦点从父元素切换到该父元素内的列表项之一,我们就失去了该样式。从 CSS 的角度来看,这是有道理的,但这不是我们想要的。

幸运的是,有一个新的 CSS 伪类可以为我们提供在这种情况下我们想要的确切内容,它被称为 :focus-within

解决方案::focus-within

伪选择器 :focus-withinCSS 选择器级别 4 规范 的一部分,并告诉浏览器在任何子元素都处于焦点状态时,对父元素应用样式。因此,在我们的例子中,这意味着我们可以切换到 **Sub-One** 并应用 :focus-within 样式以及父元素的 :hover 样式,并准确地查看我们在导航下拉菜单中的位置。在我们的例子中,它将是 ul li:focus-within > ul

ul li:hover > ul,
ul li:focus-within > ul,
ul li ul:hover {
  visibility: visible;
  opacity: 1;
  display: block;
}

太棒了!它起作用了!

快速绕道!如果您只支持现代浏览器,那么我们目前看到的 CSS 就足够了。但您应该知道,当 *任何* 浏览器不理解选择器的一部分时,它都会抛出整个选择器。因此,如果您想支持 IE 11,则不能混合使用 :focus-within 部分。

/* This compound selector will still work in IE 11 because :focus-within isn't mixed in */
ul li:hover > ul,
ul li ul:hover,
ul li ul:focus {
  visibility: visible;
  opacity: 1;
  display: block;
}

/* IE 11 won't get this, but at least the top-level menus will work */
ul li:focus-within > ul {
  visibility: visible;
  opacity: 1;
  display: block;
}

现在,当我们切换到第二个项目时,我们的子菜单会弹出,并且当我们遍历子菜单时,可见性仍然存在!现在,我们可以追加我们的代码以包含 :focus 状态以及 :hover,为键盘用户提供与鼠标用户相同的体验。

An animated screenshot of a menu showing the sybmenu being revealed when it is actively tabbed.

在大多数情况下,例如在直接链接上,我们通常只需编写如下内容

a:hover,
a:focus {
  ...
}

但在这种情况下,由于我们基于父级 li 应用悬停样式,因此我们可以再次利用 :focus-within 在通过 Tab 键进行切换时获得相同的外观和感觉。这是因为我们实际上不能 *聚焦* 到 li(除非我们添加 tabindex="0")。我们实际上正在聚焦于其中的链接 (a)。:focus-within 允许我们仍然在聚焦于链接时(非常酷!)对父级 li 应用样式。

li:hover,
li:focus-within {
  ...
}
An animated screenshot showing a tabbed menu where the submenu is revealed when actively tabbed, the submenu items show the focus ring when active, and the hover styles are also applied when active.

此时,由于我们正在应用焦点样式,因此我们可以做一些通常 不建议 的事情(删除该蓝色轮廓焦点环的样式)。我们可以通过以下方式做到这一点

li:focus-within a {
  outline: none;
}

以上代码指定当我们通过链接 (a) 将焦点放在列表项内时,不要对链接项 (a) 应用轮廓。以这种方式编写它非常安全,因为我们专门设置了悬停状态的样式,并且对于不支持 :focus-within 的浏览器,链接仍将获得焦点环。现在我们的菜单如下所示

An animated screenshot showing the final result of the tabbed menu where the focus ring has been removed and replaced by the hover state when menu items are actively tabbed.
使用 :focus-within:hover 状态以及自定义焦点环使其消失的最终菜单

ARIA 怎么样?

如果您熟悉可访问性,您可能听说过 ARIA 标签和状态。您可以利用这些优势,同时创建这些类型具有内置可访问性的下拉菜单!您可以在 此处找到一个由 Heydon Pickering 提供的极佳示例。包含 ARIA 标记时,您的代码将更像这样

<nav role="navigation">
  <ul>
    <li><a href="#">One</a></li>
    <li><a href="#" aria-haspopup="true">Two</a>
      <ul class="dropdown" aria-label="submenu">
        <li><a href="#">Sub-1</a></li>
        <li><a href="#">Sub-2</a></li>
        <li><a href="#">Sub-3</a></li>
      </ul>
    </li>
    <li><a href="#">Three</a></li>
  </ul>
</nav>

您正在向下拉菜单的父元素添加 aria-haspopup="true" 以指示替代状态,并在实际下拉菜单本身(在本例中为具有 class="dropdown" 的列表)上包含 aria-label="submenu"

这些属性本身将为您提供显示下拉菜单所需的功能,但缺点是它们仅在启用 JavaScript 时才有效。

浏览器支持注意事项

说到注意事项,让我们谈谈浏览器支持。虽然 :focus-within 确实具有 *相当不错* 的浏览器支持,但需要注意的是,Internet Explorer 和 Edge 不受支持,因此这些平台上的用户将无法看到导航。

此浏览器支持数据来自 Caniuse,其中包含更多详细信息。数字表示浏览器在该版本及更高版本中支持该功能。

桌面

ChromeFirefoxIEEdgeSafari
60527910.1

移动/平板电脑

Android ChromeAndroid FirefoxAndroidiOS Safari
12712712710.3

最终解决方案是使用 ARIA 标记和 CSS :focus-within 来确保为用户提供可靠的下拉体验。

如果您想将来能够使用此功能,请 在 Edge 用户之声上为其投票!并在您使用时 :focus-ring 投票,以便我们能够为该焦点环设置样式并为所有人创建美观的交互式 Web 体验 😀

更多关于 :focus-within 和 A11Y 的内容