Right-Click → View Page Source

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;
}

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/],
};

Join the converstation on the DEV Community or send a Webmention

About the author

A profile photo of Saul Hardman

Saul Hardman is a contract front-end web developer based in Copenhagen, Denmark. During his career he's worked with the likes of The Guardian, UNIT9, and AllofUs. As of now he's helping Realla with front-end architecture, performance, and technical SEO.

To stay up to date, follow Saul on Twitter and GitHub or subscribe to the RSS feed.