:focus-visible and backwards compatibility

Posted on Friday, 23 March 2018 by Patrick H. Lauke

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 canisue.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.

About Patrick H. Lauke

Patrick works as Senior Accessibility Engineer for The Paciello Group. He is a member of the W3C Pointer Events Working Group and W3C Touch Events Community Group. In a previous life he was a Web Evangelist in the Developer Relations team at Opera, and before that he worked as Web Editor for a large UK university for nearly 10 years. Patrick has been involved in the discourse around Web Standards and Accessibility since 2001, actively speaking at conferences and participating in initiatives such as the (now discontinued) Web Standards Project (WaSP)

Comments

  1. 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…)

  2. 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. 🙂

  3. 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.

Comments for this post are closed.