仅限CSS的无障碍下拉式导航菜单(附代码示例)

162 阅读10分钟

这项技术探讨了使用:

  • 使用CSS的动画transitiontransform
  • 使用:focus-within 伪类
  • 定位的CSS网格
  • 动态居中技术
  • 下拉菜单的可访问性考虑

如果你曾经为处理 "悬停意图 "的概念而揪心,那么这次升级是为你准备的

在我们走得太远之前,虽然我们的技术100%只使用了CSS,但为了获得更全面的无障碍体验,有必要添加一些Javascript。为了获得最可靠的支持,还需要为一个关键功能添加一个polyfill--:focus-within 。但与需要一个或多个jQuery插件来完成视觉效果的日子相比,我们还是有了很大的进步。

可访问性更新 - 08/18/20: 非常感谢Deque的Michael Fairchild(也是优秀资源a11ysupport.io的创建者)在各种辅助技术上测试了原始解决方案。只用CSS的方法需要一些Javascript来完全满足WCAG 2.1。特别是,需要使用javascript来提供一种非鼠标/非tab的方式来取消菜单(想想转义键)以满足成功标准1.4.13。迈克尔指出了这个WAI-ARIA创作实践演示,它提供了更多关于必要的Javascript功能的信息。这些都是强烈建议为您的最终生产方案增加的内容。


如果您没有使用过Sass,您可能需要花5分钟来了解Sass的嵌套语法,以便最容易理解所给的代码样本。

基础导航HTML

我们将在继续的过程中加强这一点,但这里是我们的起始结构:

<nav aria-label="Main Navigation">
  <ul>
    <li><a href="#">About</a></li>
    <li class="dropdown">
      <!-- aria-expanded needs managed with Javascript -->
      <button
        type="button"
        class="dropdown__title"
        aria-expanded="false"
        aria-controls="sweets-dropdown"
      >
        Sweets
      </button>
      <ul class="dropdown__menu" id="sweets-dropdown">
        <li><a href="#">Donuts</a></li>
        <li><a href="#">Cupcakes</a></li>
        <li><a href="#">Chocolate</a></li>
        <li><a href="#">Bonbons</a></li>
      </ul>
    </li>
    <li><a href="#">Order</a></li>
  </ul>
</nav>

稍微忽略一下button ,这是导航链接的语义标准。这种结构可以灵活地存在于页面的任何地方,因此它可以作为侧边栏的目录,就像它是主导航一样容易。

从一开始,我们就实施了一些专门针对可访问性的功能:

  1. aria-label 在 ,以帮助识别它的目的,当辅助技术被用来通过地标导航一个页面时<nav>
  2. 使用button ,作为一个可关注的、可发现的元素来触发下拉菜单的打开。
  3. aria-controls 在 ,链接到 的id,将其与辅助技术的菜单联系起来。.dropdown__title .dropdown__menu
  4. aria-expanded button ,在你的最终解决方案中,需要通过Javascript来切换,正如本文开头提到的演示中所指出的那样

正如迈克尔所指出的,使用button 元素也可以让龙语用户说出 "点击按钮 "这样的话来打开菜单。

我们的(大部分)默认起始外观如下:

default list of links

初始导航样式

首先,我们将给nav 一些容器样式,并将其定义为一个网格容器。然后我们将从nav ulnav ul li 中删除默认的列表样式:

nav {
  background-color: #eee;
  padding: 0 1rem;
  display: grid;
  place-items: center;

  ul {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;

    li {
      padding: 0;
    }
  }
}

navigation list with list styles removed

我们已经失去了分层定义,但我们可以通过以下方式开始把它带回来:

nav {
  // ...existing styles

  > ul {
    grid-auto-flow: column;

    > li {
      margin: 0 0.5rem;
    }
  }
}

通过使用子组合器选择器> ,我们已经定义了顶层的ul ,它是nav 的直接子节点,应该把它的grid-auto-flow 切换到column ,这就有效地更新了它沿x-axis 。然后我们给顶层的li 元素添加边距,以增加定义。现在,未来的下拉项出现在 "Sweets "菜单的下面,并且更明显地成为它的子项。

nav list with direct child styles

接下来,我们将首先为所有的链接以及.dropdown__title ,然后只为顶层链接以及.dropdown__title 。这也是我们清除为button 元素继承的本地浏览器风格的地方:

// Clear native browser button styles
.dropdown__title {
  background-color: transparent;
  border: none;
  font-family: inherit;
}

nav {
  > ul {
    > li {
      // All links contained in the li
      a,
      .dropdown__title {
        text-decoration: none;
        text-align: center;
        display: inline-block;
        color: blue;
        font-size: 1.125rem;
      }

      // Only direct links contained in the li
      > a,
      .dropdown__title {
        padding: 1rem 0.5rem;
      }
    }
  }
}

updated link styles

基本的下拉式样式

到目前为止,我们一直依赖元素选择器,但我们将为下拉菜单引入类选择器,因为在一个给定的导航列表中可能有多个。

我们将首先对.dropdown__menu 和它的链接进行样式设计,以便在我们通过定位和动画工作时,帮助更清楚地识别它。

.dropdown {
  position: relative;

  .dropdown__menu {
    background-color: #fff;
    border-radius: 4px;
    box-shadow: 0 0.15em 0.25em rgba(black, 0.25);
    padding: 0.5em 0;
    min-width: 15ch;

    a {
      color: #444;
      display: block;
      padding: 0.5em;
    }
  }
}

dropdown__menu styles

其中一个明显的问题是,.dropdown__menu 影响了nav 容器,你可以从灰色的nav 背景出现在下拉菜单周围看到这一点。

我们可以通过将position: absolute 添加到.dropdown__menu ,使其脱离正常的文档流程来开始解决这个问题。

menu with position absolute

你可以看到它被排列在父列表项的左边和下面。根据你的设计,这可能是理想的位置。

我们要拿出一个居中的技巧来使菜单在列表项的中央对齐:

.dropdown__menu {
  // ... existing styles

  position: absolute;

  // Pull up to overlap the parent list item very slightly
  top: calc(100% - 0.25rem);
  // Use the left from absolute position to shift the left side
  left: 50%;
  // Use translateX to shift the menu 50% of it's width back to the left
  transform: translateX(-50%);
}

这个居中技术的神奇之处在于,菜单可以是任何宽度,甚至是动态宽度,它都会适当地居中。

centered dropdown__menu styles

下拉菜单的显示方式

有两个主要的触发器我们希望用来打开菜单::hover:focus

然而,传统的:focus ,不会持久保持下拉菜单的打开状态。一旦最初的触发器失去焦点,键盘焦点可能仍然在下拉菜单中移动,但从视觉上看,菜单会消失。

:focus-within#

有一个即将到来的伪类叫做:focus-within ,它正是我们所需要的,可以使之成为一个纯CSS的下拉菜单。正如在介绍中提到的,如果你需要支持IE < Edge 79(你需要......目前),它确实需要一个polyfill

来自MDN,斜体字是为了显示我们要受益的部分。

:focus-within CSS伪类代表一个已经收到焦点的元素或包含一个已经收到焦点的元素。换句话说,它代表一个本身被:focus 伪类匹配的元素*,或者有一个被:focus 匹配的后裔。*

按默认值隐藏下拉菜单

在我们揭示下拉菜单之前,我们需要隐藏它,所以我们将使用隐藏样式作为默认状态。

你的第一直觉可能是display: none ,但这将使我们无法优雅地进行动画过渡。

接下来,你可能会尝试简单地使用opacity: 0 ,这可以明显地隐藏它,但会留下 "幽灵链接",因为该元素仍然有计算高度。

相反,我们将使用opacity,transform, 和visibilty 的组合:

.dropdown__menu {
  // ... existing styles
  transform: rotateX(-90deg) translateX(-50%);
  transform-origin: top center;
  opacity: 0.3;
}

我们添加不透明度,但不是全部为0,以便以后能有更平滑的效果。

而且,我们更新我们的transform 属性以包括rotateX(-90deg) ,这将使菜单在三维空间中 "向后 "旋转90度。这就有效地消除了高度,并将在显示时产生有趣的过渡。你也会注意到transform-origin 属性,我们添加该属性是为了更新应用变换的点,而不是默认的水平和垂直中心。

此外,为了满足成功标准1.3.2,链接应该从屏幕阅读器用户那里隐藏起来,直到它们被直观地显示出来。我们通过包括visibility: hidden (再次感谢Michael的这个提示!)来确保这种行为。

在我们进行揭示之前,我们需要添加一个transition 属性。我们把它添加到主.dropdown__menu 规则中,以便它在焦点/悬停时和关闭时都适用,也就是 "向前 "和 "向后"。

.dropdown__menu {
  // ... existing styles
  transition: 280ms all ease-out;
}

揭示下拉的内容

有了之前的所有设置,在悬停和聚焦时显示下拉菜单可以简明地完成:

.dropdown {
  // ... existing styles

  &:hover,
  &:focus-within {
    .dropdown__menu {
      opacity: 1;
      transform: rotateX(0) translateX(-50%);
      visibility: visible;
    }
  }
}

首先,我们将visibilty (否则其他属性将不起作用),然后我们将rotateX 重置为0,然后将opacity 一路提升到1 ,以便完全可见。

这就是结果:

demo of reveal on focus and hover

rotateX 属性允许菜单从后面摆动的外观,而opacity 只是使其整体过渡得更柔和。

再次说明,为了实现完全无障碍,需要Javascript完全处理键盘辅助技术事件,这些事件并不总是触发:focus 。这意味着一些有视力的键盘用户可能会发现下拉链接,但如果没有发出:focus 事件,他们就不会看到下拉菜单真正打开。查看w3c演示,了解如何在这个解决方案中完成对Javascript的整合。

处理悬停意向

如果你在网络方面已经有一段时间了,我希望下面的内容会让你觉得🤯。

当我第一次开始与下拉菜单作斗争时,我主要是为IE7创建它们。在一个大项目中,几个团队成员问了一些问题:"如果我只是在菜单上滚动/鼠标,你能不能阻止菜单出现?"。在经过大量的Google搜索(包括尝试用正确的短语来获得我想要的东西)后,我终于找到了解决方案,就是hoverIntent jQuery插件

我需要设置这个,因为我们使用的是transition 属性,我们还可以添加一个非常小的延迟。对于一般的目的,这将防止下拉动画触发 "开车 "的鼠标移动。

当我们在一行中定义所有的过渡属性时,顺序很重要,顺序中的第二个数值将被选为延迟值:

.dropdown__menu {
  // ... existing styles
  transition: 280ms all 120ms ease-out;
}

请看结果:

demo of transition delay with mouseover

它需要一个相当悠闲的翻转来触发菜单,我们可以粗略地推断出它的意图是要打开菜单。这个延迟仍然很短,在打开菜单之前不会被有意识地注意到,所以这是一个胜利

你仍然可以选择使用Javascript来加强这一点,特别是如果它要启动一个 "巨型菜单",那就更有破坏性了,但这仍然是相当令人高兴的。

下拉菜单指示器

悬停的意图是一回事,但实际上我们需要一个额外的提示,让用户知道这个菜单有额外的选项。一个非常常见的惯例是模仿本地选择元素的指示器的 "圆点 "或 "向下箭头"。

为了添加这个,我们将更新.dropdown__title 样式。我们将把它定义为一个inline-flex 容器,然后创建一个:after 元素,使用边框技巧来创建一个向下的箭头。我们使用一个破折号translateY() ,使其与我们的文本光学对齐。

.dropdown {
  // ... existing styles

  .dropdown__title {
    display: inline-flex;
    align-items: center;

    &:after {
      content: "";
      border: 0.35rem solid transparent;
      border-top-color: rgba(blue, 0.45);
      margin-left: 0.25em;
      transform: translateY(0.15em);
    }
  }
}

dropdown caret indicator

在手机上关闭菜单

这里是另一个地方,最终你可能要用Javascript来增强。

为了保持它只适用于CSS,并为非应用型网站所接受,你需要在主体上应用tabindex="-1" ,有效地允许在菜单之外的任何点击来移除焦点,并允许它关闭。

这有点牵强--而且可能会让用户有点沮丧--所以你可能想用Javascript来增强它在滚动时的隐藏功能,特别是如果你定义nav ,使用position: sticky ,并与用户一起滚动。