CSS Focus Styles for Keyboard Users Only

Hero image for CSS Focus Styles for Keyboard Users Only. Image by Yingchih.
Hero image for 'CSS Focus Styles for Keyboard Users Only.' Image by Yingchih.

There is a fine line to be trodden between the aesthetic expectations of a project, and allowing suitable accessibility. I would always argue that accessibility far outweighs any importance visuals carries, but that can be an uphill struggle in teams where the distinction between technical UX and UI aesthetics has become blurred.

We are all familiar with the situation where interactive elements onpage gain an unexpected blue glow:

  • Text inputs when clicked into;
  • Links and buttons when clicked upon.

It is these things that will have the hybrid UX/UI person frenetically walking up behind you to push their laptop in your face and frantically point at it.

Screenshot of an HTML button displaying the blue focus outline when clicked upon.

This is inherited from the :focus pseudoclass, jump onto Stack Overflow and you will find dozens of answers all saying the same thing: remove the outline, maybe even drop an !important in there for good measure.

*:focus {  outline: none !important;}

The key thing to bear in mind is that this bright outline is very important for keyboard or visually impaired users. I won't dwell on this point for too long because I hope to make it absolutely clear in a single, short sentence:

You must never just remove CSS outlines.

If you find yourself struggling against this one in a professional development environment, give them my email address. I'll explain it to them.

To placate both visuals and accessibility, there are three options you can explore:

  1. Style the outline. It doesn't actually have to be that default blue glow (which differs from browser to browser). As long as it is visually distinctive from the surrounding site, keyboard users will still be able to use it.
  2. Give the element more specific styling. You really should have a defined visual state for :focus anyway, just make sure it is distinctive enough to show that the interaction is occurring.
  3. Take the outlines away, but only for nonkeyboard users.

It is a combination of the second and third options that I most commonly implement. As I mentioned: you should already have a distinct style for your interactables anyway, remember the minimum you should be styling is default, :visited, :hover, :active, :focus. It often makes sense to combine these so that default and visited are the same, as are the other three:

Screenshot of buttons in the five pseudo-class states: default, :visited, :hover, :active, :focus.

So, with obvious enough visuals, you could argue that an outline isn't necessary at all.


Take Away Outlines for Keyboard Users Only

This is where things get a little more interesting. Although it should be a last resort, it is also possible to remove outlines for keyboard users, whilst retaining them for people traversing and interacting with the site via the keyboard.

:focusvisible is intended to do just this by allowing you to apply different focus indicators based on the user's modality (i.e., mouse, touch, or keyboard). Here's an example from the MDN documentation which I've modified a little and moved into Sass using the parent selector:

.button {  display: block;  // Provide a fallback style for browsers  // that don't support :focus-visible  &:focus {    outline: none;    background: lightgrey;    // Remove the focus indicator on mouse-focus for browsers    // that do support :focus-visible    &:not(:focus-visible) {      background: transparent;    }  }  // Draw a very noticeable focus style for keyboard-focus  // on browsers that do support :focus-visible  &:focus-visible {    outline: 4px dashed darkorange;    background: transparent;  }}

The problem here is that browser support remains very poor, and whilst there is a polyfill you could use, it is quite weighty for what is a relatively simple problem to solve.

Photograph of an older wireless Apple keyboard by Clay Banks on Unsplash.

For me, the answer comes from tying a keyboard and mouse event to the document and toggling a classname on the body:

const bodyEl = document.querySelector('body');const keyboardClass = 'keyboard-user';const handleKeydownOnce = (e) => {  const { key } = e;  if (key === 'Tab') {    bodyEl.classList.add(keyboardClass);    document.removeEventListener('keydown', handleKeydownOnce);    document.addEventListener('mousedown', handleMousedownOnce);  }};const handleMousedownOnce = (e) => {  bodyEl.classList.remove(keyboardClass);  document.removeEventListener('mousedown', handleMousedownOnce);  document.addEventListener('keydown', handleKeydownOnce);};useEffect(() => {  document.addEventListener('keydown', handleKeydownOnce);  return () => {    document.removeEventListener('keydown', handleKeydownOnce);  };});

What this does is:

  1. Use the React Effect Hook to add a keydown event listener, and remove it again on dismount. You could just as easily use vanilla JavaScript to achieve this if you aren't using React.
  2. When a key event occurs, we check event.key to determine whether it was the Tab key that was pressed or not. In the past, we could have checked to see if event.keyCode is 9 (the tab key), but keyCode has been depreciated for some time now, so it is fair to assume that support will begin to dwindle as time goes by. That said, support for KeyboardEvent.key also is not yet great so if you are supporting legacy Firefox or Internet Explorer, you may need to use a combination of key and/or code alongside keyCode.
  3. If it was a tab key, we know this is somebody attempting to traverse the page using the keyboard. Set the classname 'keyboarduser' on body, remove the keyboard event listener, and instead attach an event to listen for mouse events.
  4. If there is a mouse event, we know this user is now using the mouse instead. We remove the classname from the body, and reinstate the keyboard listener.

In this way, we minimise event listeners (which can lead to performance lag) and can determine between different interaction modalities for our users, even if they switch between keyboard and mouse.

The final piece of the puzzle is a simple piece of CSS (or Sass):

body {  &:not(.keyboard-user) {    *:focus,    *:active {      outline: none;    }  }}

This effectively removes the outline for nonkeyboard users and allows the browser default interaction highlight (assuming it has not been overridden elsewhere in your project) when using the keyboard. This is exactly how it is implemented here on my site.

As a final note on the subject: it should be considered that outlines aren't only indicative of, or for, keyboard users: the highlight allows for much easier passage through your site. Unless you are able to implement really clear visual indicators for your interactables, I would always advise great caution in overriding browser defaults.


Categories:

  1. Accessibility
  2. Cross‑Browser Compatibility
  3. CSS
  4. Development
  5. Sass