Problem

Default :focus behavior is bad.

Focus rings can be confusing for users with pointing devices (e.g. mouse, touch screen) because it implies a sense of persistence when showing up right after a click or a touch event.

Try clicking and realize how bad it is.

Designers then need to make the :focus state obvious enough for keyboard users but at the same time, keeping it unobtrusive for mouse users.

This usually involves a compromise. Some, even remove the default focusing behavior entirely which is terrible for accessibility.

Removing the :focus outline is like removing the wheelchair ramp from a school because it doesn’t fit in with the aesthetic.

David Gilbertson

Solution

Ideally, the focus ring should only show up when the user intends to use the keyboard.

We need a better default browser behavior. In cases like this, it’s always a good idea to check if there are existing or upcoming browser specifications that solve our problem.

Luckily for us, there’s one: Introducing :focus-visible 🎉.

Specification

TLDR; :focus-visible is the keyboard-only version of :focus.

Also, the W3C proposal mentions that :focus-visible should be preferred over :focus except on elements that expect a keyboard input (e.g. text field, contenteditable).

Browser Support

At the time of writing, only Firefox supports :focus-visible natively. But the good news is that there’s an excellent polyfill available for us. Here’s a demo if you’d like to see it for yourself before we dive into the actual implementation details.

Implementation

Installation

via NPM
npm install --save focus-visible
require('focus-visible')
via UNPKG
<script src="https://unpkg.com/focus-visible@latest/dist/focus-visible.min.js"></script>

Styling

The polyfill adds a .focus-visible class on elements that match the :focus-visible state, so styling is pretty straightforward.

/* Remove outline for non-keyboard :focus */
*:focus:not(.focus-visible) {
  outline: none;
}

/* Optional: Customize .focus-visible */
.focus-visible {
  outline: lightgreen solid 2px;
}
Try clicking, then tabbing (or hitting the Shift key) to see improved behavior.

⭐️ Bonus tip: a better alternative to outline

*:focus {
  outline: none;
}

/* box-shadow outlines rounded corners! */
.focus-visible {
  box-shadow: 0 0 0 2px lightgreen;
}
Ahh, rounded corners.

Noticed how I used :focus-visible on this site? Hit me up on Twitter if you find this useful! 😁