DEV Community

Cover image for Better Focus Indicators Using the `:focus-visible` Pseudo-Class
Saul Hardman
Saul Hardman

Posted on • Originally published at viewsource.io on

Better Focus Indicators Using the `:focus-visible` Pseudo-Class

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
// an entry point in your application
import "focus-visible";
Enter fullscreen mode Exit fullscreen mode
> npm install --save-dev postcss-focus-visible
Enter fullscreen mode Exit fullscreen mode
// postcss.config.js
module.exports = {
  plugins: [require("postcss-focus-visible")]
};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
        }
      ]
    })
  ]
};
Enter fullscreen mode Exit fullscreen mode

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/]
};
Enter fullscreen mode Exit fullscreen mode

Top comments (0)