:focus-visible and backwards compatibility

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;
}
Categories: Technical

Comments

Patrick H. Lauke says:

To clarify the core point, as apparently it got lost for some readers: if you care about backwards compatibility (and you should, until you can absolutely guarantee without any doubt that all your users will have a browser that supports :focus-visible), you will always have to either polyfill or use the combination of :focus and :not(:focus-visible) (plus optional even stronger :focus-visible). Because just relying on :focus-visible is not like, say, rounded corners…where users of older browsers would still get the same page, but slightly less pretty (and round). But rather, you’d be omitting visible focus indication (beyond the absolute minimum browser default, which depending on your other styling/circumstances may not be sufficiently clear or visible at all) from sighted keyboard users with those browsers.

This is not a judgement against :focus-visible – this was always a tough nut to crack. The point here is to make sure authors are very conscious of the backwards compatibility implications, and that they need to use this new pseudo-class either defensively, or by explicitly polyfilling it (and, for good measure, even if polyfilling, using the polyfill defensively as well in case it fails to load properly…)

Laurence Lewis says:

Hi Patrick
Thanks for this post. Is it possible to add, or link to, some real world samples of how this would be used. I am trying to understand the use—struggling so need a light bulb moment from any examples. 🙂

Patrick H. Lauke says:

Sorry Laurence, late reply: the tricky part of pointing out/linking to examples is that :focus-visble isn’t natively supported (yet). and it seems that, even just trying to demo this using :-moz-focusring, the following button:focus:not(:-moz-focusring) doesn’t work in Firefox either. So, for the time being, this is a bit theoretical…trying to get in early with my warning about using this new feature with care.