0

.menu > .item:nth-child(n+2) {
  display: none;
}

.menu:hover > .item,
.menu:focus > .item,
.menu > .item:focus,
.item:focus ~ .item {
  display: block;
}

.item:focus {
  outline: 3px solid red;
}
<button>Go from here</button>
<div class="menu">
  <a class="item" href="#">One</a>
  <a class="item" href="#">Two</a>
  <a class="item" href="#">Three</a>
</div>

If the cursor is over the menu, everything is tabbed back and forth fine. However, while the menu is hidden, it's not possible to focus none of the hidden elements with Tab. Is it possible to circumvent this?

Worth noting that I'd like it to work even in outdated browsers, so it's not a good idea to resort to modern solutions like :focus-within (but a JS fallback might be okay; in very old or marginal browsers, nth-child wouldn't work anyway and the menu would just be always expanded).

bodqhrohro
  • 119
  • 2
  • 7

2 Answers2

1

From the accessibility point of view, you should just stop using :hover to display or hide elements. It's bad for several reasons:

  • AS you have noticed, hidden elements aren't reachable with the keyboard. There are solutions, but you are probably going to create illogical and confusing navigation where tabbing back and forth don't make you encounter the same elements in the same order. Having a predictable tab order is crucial for keyboard only users.
  • Blind users don't have any notion of :hover at all and so may experience a capricious menu which appears and disappears without notice. Of course it is never there when you need it.
  • Partially sighted people and those with motor disability may find it difficult to precisely move the mouse to a certain point and follow a precise path, what is commonly called interaction tunnel
  • How :hover behaves on a touch screen ?

For all these reasons, it's better to forget about :hover and switch to an explicit show/hide toggle, where the state (shown or hidden) don't change without a clear user interaction.

QuentinC
  • 12,311
  • 4
  • 24
  • 37
  • That's why the solution handles BOTH hover and focus in the first place. Visually impaired users navigate the logical structure of a page rather than what is displayed on the screen, so I thought of making focusable elements which don't occupy the visible space all the time. But it turned out that modern browsers are too smart and won't allow to focus into hidden elements :/ And for touchscreen-only devices, I thought of a JS fallback which implements a toggle, as there barely are such devices with no JS, and actually it's often not even easy to disable JS there. I'll try :checked instead. – bodqhrohro Jul 24 '22 at 08:09
  • This isn't being too smart, that's just how it works: display:none means that the element is totally absent and unreachable. You can make something appear on screen when focused by putting it off-screen by default. However, it would just be easier for you and your users to have the same toggle button for everybody. Remember that one part of accessibility is also simplifying things. – QuentinC Jul 24 '22 at 11:25
  • At my point, eliminating a possibility to just hover instead of mandatorily doing two clicks for those who are able to do it is a UI degradation. ‖ I've been struggling with making a label of a hidden checkbox focusable with no JS, and finally found a better solution: https://ghinda.net/css-toggle-switch/ The trick there is to put the checkbox on an invisible layer instead of hiding it completely, and focusing it with the keyboard while still displaying the focus state with the label. – bodqhrohro Jul 24 '22 at 11:49
0

The solution is to toggle the hidden part of the menu with an invisible checkbox:

.menu {
  position: relative;
}

.menu > input {
  position: absolute;
  top: 0;
  right: 0;
  opacity: 0;
  z-index: -1;
}

.menu > .item:nth-of-type(1),
.menu > label {
  display: inline-block;
}

.menu > .item:nth-of-type(n+2) {
  display: none;
}

.menu:hover > .item:nth-of-type(n+2),
.menu > input:checked ~ .item:nth-of-type(n+2) {
  display: block;
}

.item:focus, .menu > input:focus ~ label {
  outline: 3px solid red;
}

.menu > input:checked ~ label {
  background-color: magenta;
}
<button>Go from here</button>
<div class="menu">
  <a class="item" href="#">One</a>
  <input id="c" type="checkbox" aria-label="Expand/collapse" role="button" />
  <label for="c">↓</label>
  <a class="item" href="#">Two</a>
  <a class="item" href="#">Three</a>
</div>

Thanks to @QuentinC for pointing into this direction.

bodqhrohro
  • 119
  • 2
  • 7
  • While this might be more accessible than the original solution, there still work to do. There are no roles present that would communicate that this is a menu The expected role to expand a menu item would be that of a button, not a checkbox. The button would also be expected to expose it’s state by means of `aria-expanded`. See [the Example Disclosure Navigation Menu](https://www.w3.org/WAI/ARIA/apg/example-index/disclosure/disclosure-navigation.html) for some guidance on what’s important. I’m afraid a lot of ARIA patterns simply cannot be implemented by CSS only. – Andy Jul 24 '22 at 14:39
  • I'm using Jaws with Chrome, and in your example, I can find the link "One" even when the checkbox isn't checked. Is that wanted ? For the rest it seems to work. However, please, think about setting a more explicit label than just a down arrow. It's much more important than setting the correct ARIA role and state. – QuentinC Jul 24 '22 at 19:40
  • Yes, the first item is intended to be always displayed to denote the current value, so the menu in general behaves like a dropdown, but consists of links instead. The checkbox is visible as a useless checkbox in browsers like NetFront and links2, so not a big deal really. `aria-expanded` seems to be updatable only via JS or server roundtrips, so it's out of the scope of the question, I suppose. I added some other attributes for the clarity. – bodqhrohro Jul 25 '22 at 01:57