Updated March 2023
Clearly visible focus styles are important for sighted keyboard users. However, these focus styles can often be undesirable when they are applied as a result of a mouse/pointer interaction. A classic example of this are buttons which trigger a particular action on a page, such as advancing a carousel. While it is important that a keyboard user is able to see when their focus is on the button, it can be confusing for a mouse user to find the look of the button change after they clicked it – making them wonder why the styles “stuck”, or if the state/functionality of the button has somehow changed.
For this reason, modern browsers apply simple heuristics to determine whether or not to apply their default focus styling. In general, if an element received focus as a result of a mouse/pointer click, browsers will suppress their default focus indication. (Note: some browsers use more sophisticated heuristics; for instance, Firefox won’t suppress default focus styles, even as a result of a mouse click, if the user has previously used TAB / SHIFT+TAB to navigate through the page).
When authors define explicit :focus styles, however, these browser heuristics are ignored. :focus styles are applied whenever the element receives focus, whether it was as a result of a keyboard or mouse/pointer interaction.
To circumvent this issue, authors have had to resort to hacky “solutions”, generally involving JavaScript (such as the excellent What Input?).
The recently proposed :focus-visible pseudo-class (a standardized version of Firefox’s -moz-focusring) aims to provide a standardised CSS-native solution to the problem. Instead of defining traditional :focus styles, authors would use :focus-visible, and browsers (using their built-in heuristics) would only apply those styles in the same situation as the default focus styles. Note that, at the time of writing, no browser has yet implemented :focus-visible (see caniuse.com information on :focus-visible), but if you’re “future-friendly” and planning to already use this pseudo-class to reap its benefits when browser support does come, read along…
As a basic example, let’s assume our current styles include the following:
button:focus { /* some exciting button focus styles */ }
This explicit :focus styling is currently applied whenever the button receives focus. In future, when browsers support :focus-visible, we’d instead have:
button:focus-visible { /* some exciting button focus styles */ }
While great in principle, authors won’t be able to simply replace :focus with :focus-visible, as that would break backwards compatibility and leave keyboard users with no explicit focus styling (other than whatever default the browser may still apply). Ideally then, we’d want to use :focus-visible only in browsers that support it. Unfortunately, as :focus-visible is a pseudo-class, we can’t use @supports, since feature queries don’t (currently?) support these as part of their conditions. But, even if they did, in order to cater to both non-supporting and supporting browsers, we’d essentially be defining :focus styles as normal, and then undoing those styles and replicating them again for :focus-visible. Workable, but not exactly elegant.
/* this won't actually work as @supports does not support pseudo-classes...
but it demonstrates the less than elegant style acrobatics involved in
setting and unsetting :focus styles */
button:focus { /* some exciting button focus styles */ }
@supports (:focus-visible) {
button:focus { /* undo all the above focused button styles */ }
button:focus-visible { /* and then reapply the styles here instead */ }
}
We could resort to JavaScript to try and determine support for :focus-visible (for instance, see this discussion on StackOverflow on how to detect if browsers support a specified css pseudo-class) and then dynamically swap out style definitions or entire stylesheets … but this would seem to defeat the purpose of a clean CSS-native solution.
The most viable (though still not particularly elegant) solution may be to use the :not() negation pseudo-class, and to (paradoxically) define styles not for :focus-visible, but to undo :focus styles when it is absent.
button:focus { /* some exciting button focus styles */ }
button:focus:not(:focus-visible) {
/* undo all the above focused button styles
if the button has focus but the browser wouldn't normally
show default focus styles */
}
Note that this works even in browsers that don’t support :focus-visible because although :not() supports pseudo-classes as part of its selector list, browsers will ignore the whole thing when using a pseudo-class they don’t understand/support, meaning the entire button:focus:not(:focus-visible) { ... } block is never applied.
There’s arguably some advantage in using :focus-visible if we wanted to provide additional stronger styles for browsers that support it, as we could then assume that they would only apply in the case of keyboard focus (or, more accurately, in the case where the browser’s heuristics determined that visible focus indication was appropriate). But that would still mean that in older/unsupported browsers, we’d be knowingly providing a less-than-ideal experience.
button:focus { /* some exciting button focus styles */ }
button:focus:not(:focus-visible) {
/* undo all the above focused button styles
if the button has focus but the browser wouldn't normally
show default focus styles */
}
button:focus-visible { /* some even *more* exciting button focus styles */ }
So, what does all this boil down to? Once all browsers support :focus-visible, for situation where an indication of focus as a result of a mouse/pointer click is deemed undesirable, we’d be simply be using :focus-visible where previously we used :focus. However, to support any browsers that don’t implement the pseudo-class, we’ll either have to polyfill support for :focus-visible, or always use the less than elegant :not(:focus-visible) approach to essentially unset :focus styles in situations where the browser wouldn’t set its default visible focus indication either.
Support notes — March 2023
The :focus-visible pseudo-class is now supported in all major browsers.
But we recommend that default :focus styles are still provided, as a fallback for older versions, since focus indication is so critical.
The @supports media query can be used as a progressive enhancement, via the selector() syntax:
button:focus { /* some exciting button focus styles */ }
@supports selector(:focus-visible) {
button:focus { /* undo all the above focused button styles */ }
button:focus-visible { /* and then reapply the styles here instead */ }
}
However using the negation approach via :not(:focus-visible) is probably the better solution, because it doesn’t require defining the the focus styles twice. This could be defined on a per-rule basis, or it could be defined as a universal selector, to negate all focus styles when :focus-visible doesn’t apply:
*:focus:not(:focus-visible) {
outline: none !important;
}