CodeSpud

Fix accessibility using focus-within

January 08, 2023

accessibilitycssfrontend

A few issues I found recently for clients were related to accessibility. A few perfectly working user interfaces were not keyboard accessible. Not because they are custom controls nor because the controls were not tabable. But, because the keyboard experience did not match the mouse-only interactions. These user interfaces are confusing, to say the least.

I solved these issues by using the :focus-within psuedo-selector.

I listed a few of them and the solutions.

Table row with hidden buttons

Here the developer added an effect that shows the buttons when hovered. The markup is accessible to screen readers but does not look correct for sighted persons that use the keyboard. Accessibility is not just for screenreaders. Sighted people need to make sense of the page too.

Checkbox
Name Job description
Controls
John Doe rocket scientist
Adam smith economist
Jose novelist
  <table tabindex="0">
    <thead>
      <tr>
        <th>
          <div class="sr-only">Checkbox</div>
        </th>
        <th>Name</th>
        <th>Job description</th>
        <th>
          <div class="sr-only">Controls</div>
        </th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td><input type="checkbox" value="1" name="user"></td>
        <td>John Doe</td>
        <td>rocket scientist</td>
        <td>
          <button class="delete" aria-label="delete"></button>
          <button class="save" aria-label="save">💾</button>
        </td>
      </tr>
      ...
    </tbody>
  </table>

Using the mouse, the table above works perfectly well. There is color highlighting and the buttons become visible when the mouse is over a table row.

If you navigate the table using the keyboard, the experience is not what you will expect. Firstly, you don’t see the highlight color when focused on a row. The worst of it is you will be able to go through all the focusable elements using the keyboard - including the invisible buttons. This is confusing for sighted people using the keyboard.

Let’s check the style code below. The buttons are invisible by default(opacity: 0). When the mouse is over the row (:hover), it gets a background color and buttons become visible(opacity: 100%).

// SCSS

tbody tr {
  button {
    opacity: 0;
  }

  /**
  * Show buttons on hover
  */
  &:hover {
    background-color: #ffd57e;
    button {
      opacity: 100%;
    }
  }
}

Solution

We can use javascript and add an event handler on the focusable elements that gets triggered whenever any element on the row is focused. With that we can set a class (.has-focused-element) that applies the same style above.

const els = document.querySelectorAll('tr input[type=checkbox], tr button');

els.forEach(el => document.addEventListener('focus', onFocus));

// etc...
// SCSS

tbody tr {
  // ...

  /**
  * Show buttons on hover
  */
  &:hover,
  &.has-focused-element {
    background-color: #ffd57e;
    button {
      opacity: 100%;
    }
  }
}

The changes above will work but uneccessary. We should try to solve any style problems inside the CSS first before reaching for Javascript.

Another solution is to use :focus-within where the :hover psuedo-selector is defined.

The :focus-within CSS pseudo-class matches an element if the element or any of its descendants are focused. In other words, it represents an element that is itself matched by the :focus pseudo-class or has a descendant that is matched by :focus. (This includes descendants in shadow trees.) - MDN

“Focus-within” psuedo selector can match elements that has elements that gets focused. In this case, we have the checkbox and button elements. When these elements gets focus, we can apply the same style as if we hovered the pointer on the table row. This will work without adding a line of javascript.

// SCSS

tbody tr {
  button {
    opacity: 0;
  }

  /**
  * Show buttons on hover and when an element is focused inside the row
  */
  &:hover,
  &:focus-within {
    background-color: #ffd57e;
    button {
      opacity: 100%;
    }
  }
}

Solution in action

Here’s the same table using :focus-within,

Checkbox
Name Job description
Controls
John Doe rocket scientist
Adam smith economist
Jose novelist

A user can navigate through the focusable elements in the row and see the “hover” effect.

Card with like(♥) button

Here’s another user interface with a hover effect but no keyboard focus effect. Hovering over any part of the card applies a heart icon on top of the image indicating to the user that they can make the image their favorite. This card UI is part of an image gallery.

Works when using the mouse but when navigating using the keyboard - again the user experience fails. It lacks a good keyboard experience. When you tab through the card, the button gets focused - which is fine - but it’s not the effect we wanted.

<div class="card">
    <img src="https://images.unsplash.com/photo-1671026423293-7adf6a6abd13?crop=entropy&cs=tinysrgb&fm=jpg&ixid=MnwzMjM4NDZ8MHwxfHJhbmRvbXx8fHx8fHx8fDE2NzMwNTEwNzE&ixlib=rb-4.0.3&q=80" alt="">
    <span></span>
    <div class="button-group">
      <button class="like"></button>
    </div>
  </div>
</div>
// scss

.card {
  
  // ...
  
  &:hover {
    img {
      opacity: 70%;
      background-color: #000;
    }

    span {
      opacity: 100%;
    }

    button {
      opacity: 0;
    }
  }

  // ...
}

I know some will ask why can we not just add tabindex=0 on the card and add a :focus style. Yeah that could work. But, we would end up with two focus steps(one for the card and the heart (♥) button) instead of just the button. We don’t want that.

Solution

Again :focus-within to the rescue. We go back to the style code and look for :hover styling.

// scss
/**
  * Make heart cover image visible when button is focused
  */
  &:hover,
  &:focus-within {
    img {
      opacity: 70%;
      background-color: #000;
    }

    span {
      opacity: 100%;
    }

    button {
      opacity: 0;
    }
  }

The changes above should apply the same experience for both mouse and keyboard users.

As a rule, we also want to add an outline when the card is focused using the keyboard. Similar to what we get when we focus any interactive control with the keyboard.

// scss
.card {
  // ...
  
  &:focus-within {
    outline: 4px dashed #333;
    outline-offset: 2px;
  }

  //...
}

Solution in action

Use the Tab key to navigate through the card. When the button gets focused the card activates the hover effect we defined earlier. Now both hover and focus events show the same effect.

Apply effects when buttons are hovered

In this example we have an image with a color swatch control. When you hover over any of the colors, the corresponding color value is blended with the image. This works correctly with the mouse but with the keyboard. We want to be able to get the same effect on the image when any of the buttons gets focused using the keyboard.

  <div class="picker">
    <div class="picker-group">
      <button class="picker-item" style="--selected-color: #912424"><span class="sr-only">red</span></button>
      <button class="picker-item" style="--selected-color: #3566b2"><span class="sr-only">blue</span></button>
      <button class="picker-item" style="--selected-color: #226642"><span class="sr-only">green</span></button>
    </div>
  </div>
// scss
.picker {

  // ...
  
  background-image: url(https://images.unsplash.com/photo-1552944150-6dd1180e5999?crop=entropy&cs=tinysrgb&fm=jpg&ixid=MnwzMjM4NDZ8MHwxfHJhbmRvbXx8fHx8fHx8fDE2NzMwNTkzNzg&ixlib=rb-4.0.3&q=80);
  background-color: var(--selected-color);

  // ...

  &:hover {
    // blend image with color when button is hovered or focused
    background-blend-mode: hard-light;
  }

  // ...
}

For this one we have some scripting to setup the hover(mouseover) effect. We try set the CSS variable --selected-color as the picker background color.

const picker = document.querySelector(".picker");
const buttons = document.querySelectorAll(".picker-item");

function onHighlight(e) {
  const target = e.currentTarget;
  picker.style = target.style.cssText;
}

function onRemoveColor() {
  picker.style = "";
}

buttons.forEach((button) => {
  button.addEventListener("mouseover", onHighlight);
});

picker.addEventListener("mouseout", onRemoveColor);

This is how it looks. My blog software does not run javascript so the UI below only shows a snapshot of one of the button focused. I will include a Codepen demo later in this page so you can interact with it.

Again the issue here is that the interface does not work well with the keyboard for sighted people. It’s screen reader friendly but the experience is lacking when not using the mouse.

This is a bit more complicated so we leverage :focus-within in conjunction with a focus event handler to set the color and blend style when using the keyboard.

// SCSS

.picker {
  // ...

  &:hover,
  &:focus-within {
    // blend image with color when button is hovered or focused
    background-blend-mode: hard-light;
  }
}
// ...

buttons.forEach((button) => {
  button.addEventListener("mouseover", onHighlight);
  button.addEventListener("focus", onHighlight); // trigger highlight script when buttons are focused
});

//...

Using the solution above, we get a better experience without a lot of extra code. See the solution in action in the Codepen below.

Full solution

Here are all three solutions in action. Check out the final code to understand the solutions in their entirety.

Wrapping up

Sometimes as web developers, it is great to work on problems that improve the user experience for a variety of people. For all the tools in our disposal its really great to know that adding a single psuedo-selector like :focus-within could be so useful. Without it, we might have ended up with some complicated solutions or worst case have to start from scratch.

Reference

By @codespud  
DISCLAIMER This is my personal weblog and learning tool. The content within it is exactly that – personal. The views and opinions expressed on the posts and the comments I make on this Blog represent my own and not those of people, institutions or organisations I am affiliated with unless stated explicitly. My Blog is not affiliated with, neither does it represent the views, position or attitudes of my employer, their clients, or any of their affiliated companies.