Better Focus Indicators Using the `:focus-visible` Pseudo-Class
Tags: html, css, a11y, postcss
Published: 2020-02-13 14:00:07

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:-

```css
/* 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](https://drafts.csswg.org/selectors-4/#the-focus-visible-pseudo) 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.

```css
: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](https://caniuse.com/#search=focus-visible), 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](https://github.com/WICG/focus-visible) and [PostCSS plugin](https://github.com/jonathantneal/postcss-focus-visible) polyfill the behaviour and transpile the syntax respectively.

```bash
> npm install --save focus-visible
```

```js
// an entry point in your application
import "focus-visible";
```

```bash
> npm install --save-dev postcss-focus-visible
```

```js
// postcss.config.js
module.exports = {
  plugins: [require("postcss-focus-visible")]
};
```

**Note**: I'd recommend configuring this plugin in the context of [PostCSS Preset Env](https://preset-env.cssdb.org/) to tie this transformation to a [Browserslist](https://github.com/browserslist/browserslist) support configuration.

There is one thing to keep in mind when using the `:focus-visible` pseudo-class selector; the [CSS specification](https://stackoverflow.com/a/38873378/3081147) 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):-

```css
/* 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`](https://cssnano.co/), in particular, is not yet aware that merging rules with invalid selectors is not a safe transformation to make (see [this GitHub issue](https://github.com/cssnano/cssnano/issues/642) for updates). Until this issue is resolved it's possible to disable the `mergeRules` option:-

```js
// postcss.config.js
modules.exports = {
  plugins: [
    require("cssnano")({
      preset: [
        "default",
        {
          mergeRules: false
        }
      ]
    })
  ]
};
```

Additionally, if [PurgeCSS](https://purgecss.com/) is part of your build pipeline, it's necessary to whitelist selectors that include the class name that's dynamically added by the polyfill:-

```js
// purgecss.config.js
module.exports = {
  whitelistPatterns: [/\.focus-visible/],
  whitelistPatternsChildren: [/\.focus-visible/]
};
```