Balancing accessible focus styles and the designer's perfection

8 September 2020

The focus outline is always a nightmare for designers. Removing it is even worse. How to make everybody happy.

Table of contents

  1. The problem
  2. The good solution
  3. The workaround solution
  4. The future solution

The problem

The problem is known as one of the worst practices when you ask any developer with at least a minimum knowledge of accessibility. It appears to be an ever returning discussion between me and designers. I never met a designer who liked the default focus style. What I am talking about is this:

*:focus {
  outline: none;
}

The selector here selects any element having the focus. "Focus" means the element in the browser that is currently in an active state, ready to be pressed, inputted into or similar. When you use a mouse, you normally know which element is in focus, because it is the one that was last clicked.

However, if you are using a keyboard to navigate the site (using the "tab" key), you need to somehow know where you are on the page. The outline that is removed in the code above, is therefore a necessary visual indication for users who navigate the site with their keyboard. That's why we should never remove it just like that.

The good solution

What we can do instead (and I would always suggest) is replacing the default style (outline) with another style more suitable for the given design. This can be anything as long as it is clearly visible and does not rely on color only. It's really that simple.

button {
  color: green;
  border: 2px solid transparent;
  border-radius: 15px;
}

button:focus {
  outline: none;
  border-color: green;
}

The workaround solution

But what if - for whatever reason - you can't add another style or the designer still asks you to remove that weird outline. Please don't just give up and do what they say.

To get rid of the focus indicators in case the user does not depend on them (as is normally the case for mouse users), you can check what type of input device is used and then decide whether or not to keep the styles.

Here is how I do that. I add a class to the body element as soon as the mousemove event is called and remove it again as soon as the tab key was pressed (keydown).

const mouseUserClass = 'mouse-user';
const mouseEvent = 'mousemove';
const keyboardEvent = 'keydown';
const tabKeyCode = 9;

const handleMouse = () => {
  document.body.classList.add(mouseUserClass);

  window.removeEventListener(mouseEvent, handleMouse);
  window.addEventListener(keyboardEvent, handleKeyboard);
});

const handleKeyboard = (event) => {
  const keyCode = event.keyCode || event.which;

  if (keyCode === tabKeyCode) {
    document.body.classList.remove(mouseUserClass);
    
    window.removeEventListener(keyboardEvent, handleKeyboard);
    window.addEventListener(mouseEvent, handleMouse);
  }
}

window.addEventListener(mouseEvent, handleMouse);

I initialize this by adding only the mouse event listener, because that way as long as no class is set, no styles are removed and therefore the default behaviour is preserved. Of course you could also do it the other way around. Just be careful with assuming mouse users as default, because of course there are many other input types except mouse and keyboard.

The reason why I ask for the keyCode is that if you are mainly a mouse user you don't want the styles to switch back and forth, when typing in a form for example, because your input device is changing all the time. Instead I only switch back to "keyboard-mode", when the tab key is pressed, because that is how you navigate through the elements on a site.

Now that the classes are set, I can go and add (or better: remove) the styles I (don't) want.

body.mouse-user {
  input,
  textarea,
  select,
  button,
  a {
    &:focus {
      outline: none;
    }
  }
}

Note that I am using SCSS here, so this is not valid CSS syntax.

Of course this is not a perfect solution. It's just a very quick workaround, when you really don't have the time to style focus states for all elements.

The future solution

The good news is that there already is a much simpler solution for that, it is just not supported in all browsers yet.

:focus-visible does almost the same, but more accurate and requires only CSS:

The :focus-visible pseudo-class applies while an element matches the :focus pseudo-class and the UA (User Agent) determines via heuristics that the focus should be made evident on the element. (Many browsers show a “focus ring” by default in this case.)

More blog posts