CSS 变得越来越强大,并且随着 CSS 网格和自定义属性(也称为 CSS 变量)等功能的出现,我们看到了许多真正有创意的解决方案。其中一些解决方案不仅专注于使网络更美观,还专注于使网络更易于访问,并改善易于访问的样式体验。我非常赞同!
文章系列
- 为 SVG 背景着色
- 下拉菜单(本文)
- 基于给定元素数量的逻辑样式
我们在网络上看到的一种常见的 UI 模式是下拉菜单。它们用于分段显示相关信息,而不会让用户感到按钮、文本和选项过多。我们经常在网站的页眉或导航区域中看到它们。

让我们看看是否可以仅使用 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** 时,您会看到一个焦点指示器环,但是当您切换到下一个项目(其子菜单项之一)时,该焦点会消失。

现在,需要注意的是,理论上您正在聚焦于此其他项目,并且屏幕阅读器能够解析它,读取 **Sub-One**,但键盘用户将无法看到发生了什么,并且会失去跟踪。
发生这种情况的原因是,虽然我们正在设置父元素的悬停样式,但一旦我们将焦点从父元素切换到该父元素内的列表项之一,我们就失去了该样式。从 CSS 的角度来看,这是有道理的,但这不是我们想要的。
幸运的是,有一个新的 CSS 伪类可以为我们提供在这种情况下我们想要的确切内容,它被称为 :focus-within
。
:focus-within
解决方案:伪选择器 :focus-within
是 CSS 选择器级别 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
,为键盘用户提供与鼠标用户相同的体验。

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

此时,由于我们正在应用焦点样式,因此我们可以做一些通常 不建议 的事情(删除该蓝色轮廓焦点环的样式)。我们可以通过以下方式做到这一点
li:focus-within a {
outline: none;
}
以上代码指定当我们通过链接 (a
) 将焦点放在列表项内时,不要对链接项 (a
) 应用轮廓。以这种方式编写它非常安全,因为我们专门设置了悬停状态的样式,并且对于不支持 :focus-within
的浏览器,链接仍将获得焦点环。现在我们的菜单如下所示

: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,其中包含更多详细信息。数字表示浏览器在该版本及更高版本中支持该功能。
桌面
Chrome | Firefox | IE | Edge | Safari |
---|---|---|---|---|
60 | 52 | 否 | 79 | 10.1 |
移动/平板电脑
Android Chrome | Android Firefox | Android | iOS Safari |
---|---|---|---|
127 | 127 | 127 | 10.3 |
最终解决方案是使用 ARIA 标记和 CSS :focus-within
来确保为用户提供可靠的下拉体验。
如果您想将来能够使用此功能,请 在 Edge 用户之声上为其投票!并在您使用时 为 :focus-ring
投票,以便我们能够为该焦点环设置样式并为所有人创建美观的交互式 Web 体验 😀
:focus-within
和 A11Y 的内容
更多关于 - Scott O’Hara 撰写了关于
:focus-within
的文章,重点介绍了诸如突出显示的<table>
行和下拉菜单之类的演示 - Kushagra Gour 在 创建焦点捕获模式 上
- Eric Bailey 在 关于焦点样式的总体介绍 上
- Chris 在 当子元素获得焦点时保持父元素可见 上
- CSS-Tricks 上所有与
:focus-within
相关的文章
有一个
:focus-within
的 polyfill https://github.com/jonathantneal/focus-within它的工作原理类似于
:focus-visible
的 polyfill,需要您使用回退属性或类名。同时,您可以使用 CSS 标准的方式编写它,并使用 https://github.com/jonathantneal/postcss-focus-within 为您添加回退选择器。嘿,很棒的文章!
当从子菜单悬停移出,然后再次悬停到该区域时,会出现一个小问题,子菜单会再次可见,而无需悬停父项。对于许多子菜单和更多嵌套来说,这可能有点烦人。
我会添加
display: none;
属性到ul li ul
和display: block;
到ul li:hover > ul
来解决它。好点子!我已经在 Codepen 示例中添加了它。
您还可以使用
pointer-events: none;
在ul li ul
上,然后使用pointer-events: all;
在ul li:hover > ul
上,以防止display: none;
影响您在子菜单上使用的任何类型的过渡。您好,这篇文章很棒,有很好的技巧,您能否在菜单下拉之前添加一个过渡延迟?如果您不想与菜单交互,但您的光标悬停在上面,它不会出现,这在您的示例中不是这种情况。
这对我不起作用在 Firefox 中。我在 Chrome 中打开它,一切正常。我真的很喜欢这些教程和这个页面。感谢分享。
很棒的东西!使用 aria-expanded 会提高可访问性吗?
感谢您包含可访问性支持!然而,作为键盘用户,一件令人头疼的事情是被迫在选项卡切换时遍历所有子菜单项。通常对于较大的巨型菜单,您需要做更多工作才能获得良好的用户体验:要么将整个菜单视为一个选项卡停止点并使用方向键,要么只使顶级项目可选项卡化。然后可以使用 TAB 跳过子项目,但可以使用 Enter 键访问以启用子菜单,并在打开时使用方向键到达每个子项目(Target.com 这样做)。这有助于键盘效率,避免被迫遍历所有子项目。
好的,Marcy。是否有与此相关的代码示例,并与上面的示例代码相关联?
WCAG 规范中提供的另一个选项是提供跳至内容链接。我们将其设置为仅在键盘导航时作为选项卡顺序中的第一个项目显示给用户。
这是我刚刚在我的网站上发布的 文章和 Codepen 示例,使用 Marcy 的计划。
menu
和command
怎么办?太棒了!这就是我在评论中提到的意思:https://css-tricks.org.cn/keeping-parent-visible-child-focus/#comment-1613750
在悬停时保持父菜单的颜色状态就像组合
nav li a:focus, nav li a:hover
和nav li:hover > a
一样简单。但是,在失去焦点状态后保持子菜单可见而不使用 JavaScript 则是另一种情况。一篇很棒的文章,包含简洁的示例以及最重要的易读代码。
不过,一个小小的说明。
据我所知,您不需要向
<nav>
HTML 元素添加role="navigation"
,因为它本身就定义了导航界标(没有任何role
)。来自 https://www.w3.org/TR/wai-aria-practices/#aria_lh_navigation
有一件事我不明白,在您的 CSS 中,为什么您在
ul li ul li
上添加了clear: both
,您似乎没有使用任何浮动?为了获得更好的浏览器支持,您可以执行以下操作,而不是使用 :focus-within..
[code]a:focus + ul {
…
}[/code]
不要写 li:hover, li:focus-within。IE 不支持 :focus-within。因此,:hover 将不起作用。最好使用 li:hover{/styles/} li:focus-within{/styles/}。
看起来 Heydon 已经更新了他对菜单/导航组件以及可访问性模式的建议。在普通网站导航的情况下,他建议不要使用 aria-haspopup 属性。
https://inclusive-components.design/menus-menu-buttons/
此外,在 nav 元素上使用 role=”navigation” 可能没有必要,因为它是由浏览器隐式设置的。
继续努力!
下拉菜单在 iOS 9.3.5 的 Safari 或 Chrome 中不起作用。我没有更新的 iOS 可以测试。在最新的 Android 上的 Chrome 中完美运行,包括点击任意位置以关闭。
很棒的技巧 Una,我可以看到一些微交互的可能性(例如输入框中的浮动标签)。
对于像导航这样核心的组件,由于浏览器支持以及 @marcysutton 提到的键盘可访问性问题,我认为这不是最佳流程,也不推荐使用 W3C
感谢分享。
不幸的是,当向后按 Tab 键(Shift + Tab)时,它不会以理想的方式工作,因为 Tab 顺序“跳过”了子项目。为了实现可访问性,在两个方向上都期望 Tab 顺序相同。