It used to be common practice in web design to "nuke from orbit" the default
(and admittedly somewhat jarring) browser styles of the :focus
pseudo-class
selector.
Even popular CSS styling resets included rules such as this:-
/* remember to define focus styles! */
:focus {
outline: 0;
}
Speaking from experience, it was often the case that when designers were conducting a review they would express distaste when clicking on links and buttons left lingering dotted outlines and glowing gradients.
Don't get me wrong, I'm not trying to pass the buck here. None of us were aware of how important these focus indicators were to assistive technologies, but it's less forgivable in Today's burgeoning culture of inclusivity and accessibility – and rightly so.
Despite changing our ways – ensuring that :focus
styles got the same attention
as :hover
and its siblings – the question of whether a tap or a click should
.focus()
an element still remained. There was no practical way to
differentiate between this focus scenario and that of an assistive tool or
keyboard navigation, until now...
:focus-visible
is a new
CSS pseudo-class
selector that's currently in the draft stages.
The
:focus-visible
pseudo-class applies while an element matches the:focus
pseudo-class and the user agent determines via heuristics that the focus should be made evident on the element.
:focus-visible {
outline: 2px solid gold;
outline-offset: 2px;
}
:focus:not(:focus-visible) {
outline: none;
}
As :focus-visible
is only in the draft stages, and support is currently
particularly poor, it's necessary
to polyfill the behaviour using JavaScript.
Ideally, we'd author styles as if the feature was supported and have a JavaScript module and PostCSS plugin polyfill the behaviour and transpile the syntax respectively.
> npm install --save focus-visible
// an entry point in your application
import 'focus-visible';
> npm install --save-dev postcss-focus-visible
// postcss.config.js
module.exports = {
plugins: [require('postcss-focus-visible')],
};
Note: I'd recommend configuring this plugin in the context of PostCSS Preset Env to tie this transformation to a Browserslist support configuration.
There is one thing to keep in mind when using the :focus-visible
pseudo-class
selector; the CSS specification
dictates that browsers nullify entire rules that contain unknown or invalid
selectors. Accordingly, we must create separate rules for all :focus-visible
styles (at least until browser support improves):-
/* invalid */
button:hover,
button:focus-visible {
color: white;
background-color: blue;
}
/* valid */
button:hover {
color: white;
background-color: blue;
}
button:focus-visible {
color: white;
background-color: blue;
}
Another issue to be aware of is how these styles are handled by CSS compression
tools. cssnano
, in particular, is not yet aware that
merging rules with invalid selectors is not a safe transformation to make (see
this GitHub issue for updates).
Until this issue is resolved it's possible to disable the mergeRules
option:-
// postcss.config.js
modules.exports = {
plugins: [
require('cssnano')({
preset: [
'default',
{
mergeRules: false,
},
],
}),
],
};
Additionally, if PurgeCSS is part of your build pipeline, it's necessary to whitelist selectors that include the class name that's dynamically added by the polyfill:-
// purgecss.config.js
module.exports = {
whitelistPatterns: [/\.focus-visible/],
whitelistPatternsChildren: [/\.focus-visible/],
};