Enable :focus only on keyboard use (or tab press)

Update: This issue may no longer be relevant

Some other posters have mentioned the :focus-visible pseudo class – which now has decent browser support

I would like to add, that based on the spec which covers the :focus-visible pseudo class, browsers should now only indicate focus when it is helpful to the user – such as in cases where the user interacts with the page via a keyboard or some other non-pointing device

This basically means that the original issue is no longer relevant, because now, when a user clicks/taps a button (or another focusable element), the User Agent won’t show the focus ring anymore – even though the button is focused – because in this case the focus ring isn’t helpful to the user.

From the spec:

While the :focus pseudo-class always matches the currently-focused
element, UAs only sometimes visibly indicate focus (such as by
drawing a “focus ring”), instead using a variety of heuristics to
visibly indicate the focus only when it would be most helpful to the
user. The :focus-visible pseudo-class matches a focused element in
these situations only…

Indeed, as of version 90, Chromium’s User Agent stylesheet switched from :focus to :focus-visible, and as a result of this change, button clicks and taps no longer invoke focus rings

Also, as of version 87, Firefox also uses :focus-visible on their User Agent style.

All that being said, if custom focus styles are needed, since focus styles have now shifted from :focus to :focus-visible, when overriding the default styles with custom focus styles – the :focus-visible pseudo class should be used.

Something like this:

button:focus-visible {
  /* remove default focus style */
  outline: none;
  /* custom focus styles */
  box-shadow: 0 0 2px 2px #51a7e8;
  color: lime;
}

Backwards Compatibility:

The possible problem with using :focus-visible like this, is that browsers which don’t support :focus-visible, will show the default focus ring, which may not be clear or visible – depending on the design.

Šime Vidas, in this article, describes a viable strategy to currently use the :focus-visible pseudo class – which would work even in browsers which don’t yet support :focus-visible –

A good way to start using :focus-visible today is to define the focus
styles in a :focus rule and then immediately undo these same styles in
a :focus:not(:focus-visible) rule. This is admittedly not the most
elegant and intuitive pattern, but it works well in all browsers:

Browsers that don’t support :focus-visible use the focus styles
defined in the :focus rule and ignore the second style rule completely
(because :focus-visible is unknown to them).

In browsers that do support :focus-visible, the second style rule
reverts the focus styles defined in the :focus rule if the
:focus-visible state isn’t active as well. In other words, the focus
styles defined in the :focus rule are only in effect when
:focus-visible is also active.

button:focus {
  outline: none;
  background: #ffdd00; /* gold */
}

button:focus:not(:focus-visible) {
  background: white; /* undo gold */
}

Original Answer:

This excellent article by Roman Komarov poses a viable solution for achieving keyboard-only focus styles for buttons, links and other container elements such as spans or divs (which are artificially made focusable with the tabindex attribute)

The Solution:

button {
  -moz-appearance: none;
  -webkit-appearance: none;
  background: none;
  border: none;
  outline: none;
  font-size: inherit;
}

.btn {
  all: initial;
  margin: 1em;
  display: inline-block; 
}

.btn__content {
  background: orange;
  padding: 1em;
  cursor: pointer;
  display: inline-block;
}


/* Fixing the Safari bug for `<button>`s overflow */
.btn__content {
    position: relative;
}

/* All the states on the inner element */
.btn:hover > .btn__content  {
    background: salmon;
}

.btn:active > .btn__content  {
    background: darkorange;
}

.btn:focus > .btn__content  {
    box-shadow: 0 0 2px 2px #51a7e8;
    color: lime;
}

/* Removing default outline only after we've added our custom one */
.btn:focus,
.btn__content:focus {
    outline: none;
}
<h2>Keyboard-only focus styles</h2>

<button id="btn" class="btn" type="button">
    <span class="btn__content" tabindex="-1">
        I'm a button!
    </span>
</button>

<a class="btn" href="#x">
    <span class="btn__content" tabindex="-1">
        I'm a link!
    </span>
</a>

<span class="btn" tabindex="0">
    <span class="btn__content" tabindex="-1">
        I'm a span!
    </span>
</span>

<p>Try clicking any of the the 3 focusable elements above - no focus styles will show</p>
<p>Now try tabbing - behold - focus styles</p>

Codepen

  1. Wrap the content of the original interactive element inside an additional inner element with tabindex="-1" (see explanation below)

So instead of say:

<button id="btn" class="btn" type="button">I'm a button!</button>

do this:

<button id="btn" class="btn" type="button">
    <span class="btn__content" tabindex="-1">
        I'm a button!
    </span>
</button>
  1. Move the css styling to the inner element (layout css should remain on the original outer element) – so the width / height of the outer element come from the inner one etc.

  2. Remove default focus styling from both outer and inner elements:

    .btn:focus,
    .btn__content:focus {
    outline: none;
    }

  3. Add focus styling back to the inner element only when the outer element has focus:

    .btn:focus > .btn__content {
    box-shadow: 0 0 2px 2px #51a7e8; /* keyboard-only focus styles /
    color: lime; /
    keyboard-only focus styles */
    }

Why does this work?

The trick here is setting the inner element with tabindex="-1" – see MDN:

A negative value (usually tabindex=”-1″ means that the element should
be focusable, but should not be reachable via sequential keyboard
navigation…

So the element is focusable via mouse clicks or programatically, but on the other hand – it can’t be reached via keyboard ‘tabs’.

So when the interactive element is clicked – the inner element gets the focus. No focus styles will show because we have removed them.

.btn:focus,
.btn__content:focus {
    outline: none;
}

Note that only 1 DOM element can be focused at a given time (and document.activeElement returns this element) – so only the inner element will be focused.

On the other hand: when we tab using the keyboard – only the outer element will get the focus (remember: the inner element has tabindex=”-1″ and isn’t reachable via sequential keyboard navigation) [Note that for inherently non-focusable outer elements like a clickable <div> – we have to artificially make them focusable by adding tabindex="0"]

Now our CSS kicks in and adds the keyboard-only focus styles to the inner element.

.btn:focus > .btn__content  {
    box-shadow: 0 0 2px 2px #51a7e8; /* keyboard-only focus styles */
    color: lime; /* keyboard-only focus styles */
} 

Of course, we want to make sure that when we tab and press enter – we haven’t broken our interactive element and the javascript will run.

Here is a demo to show that this is indeed the case, note though that you only get this for free (ie pressing enter to cause a click event) for inherently interactive elements like buttons and links… for other elements such as spans – you need to code that up manually 🙂

//var elem = Array.prototype.slice.call(document.querySelectorAll('.btn'));
var btns = document.querySelectorAll('.btn');
var fakeBtns = document.querySelectorAll('.btn[tabindex="0"]');


var animate = function() {
  console.log('clicked!');
}

var kbAnimate = function(e) {
  console.log('clicking fake btn with keyboard tab + enter...');
  var code = e.which;
  // 13 = Return, 32 = Space
  if (code === 13) {
    this.click();
  }  
}

Array.from(btns).forEach(function(element) {
  element.addEventListener('click', animate);
});

Array.from(fakeBtns).forEach(function(element) {
  element.addEventListener('keydown', kbAnimate);
});
button {
  -moz-appearance: none;
  -webkit-appearance: none;
  background: none;
  border: none;
  outline: none;
  font-size: inherit;
}

.btn {
  all: initial;
  margin: 1em;
  display: inline-block; 
}

.btn__content {
  background: orange;
  padding: 1em;
  cursor: pointer;
  display: inline-block;
}


/* Fixing the Safari bug for `<button>`s overflow */
.btn__content {
    position: relative;
}

/* All the states on the inner element */
.btn:hover > .btn__content  {
    background: salmon;
}

.btn:active > .btn__content  {
    background: darkorange;
}

.btn:focus > .btn__content  {
    box-shadow: 0 0 2px 2px #51a7e8;
    color: lime;
}

/* Removing default outline only after we've added our custom one */
.btn:focus,
.btn__content:focus {
    outline: none;
}
<h2>Keyboard-only focus styles</h2>

<button id="btn" class="btn" type="button">
    <span class="btn__content" tabindex="-1">
        I'm a button!
    </span>
</button>

<a class="btn" href="#x">
    <span class="btn__content" tabindex="-1">
        I'm a link!
    </span>
</a>

<span class="btn" tabindex="0">
    <span class="btn__content" tabindex="-1">
        I'm a span!
    </span>
</span>

<p>Try clicking any of the the 3 focusable elements above - no focus styles will show</p>
<p>Now try tabbing + enter - behold - our interactive elements work</p>

Codepen


NB:

  1. Although this seems like an overly-complicated solution, for a non-javascript solution it’s actually quite impressive. Simpler css-only ‘solutions’ involving :hover and :active pseudo class styling simply don’t work. (unless of course you assume that the interactive element disappears immediately on click like a button within a modal say)
button {
  -moz-appearance: none;
  -webkit-appearance: none;
  background: none;
  border: none;
  font-size: inherit;
}

.btn {
  margin: 1em;
  display: inline-block; 
  background: orange;
  padding: 1em;
  cursor: pointer;
}

.btn:hover, .btn:active {
  outline: none;
}
<h2>Remove css :focus outline only on :hover and :active states</h2>

<button class="btn" type="button">I'm a button!</button>

<a class="btn" href="#x">I'm a link!</a>

<span class="btn" tabindex="0">I'm a span!</span>

<h3>Problem: Click on an interactive element.As soon as you hover out - you get the focus styling back - because it is still focused (at least regarding the button and focusable span) </h3>

Codepen

  1. This solution isn’t perfect: firefox on windows will still get focus styles for buttons on click – but that seems to be a firefox bug (see the article)

  2. When browsers implement the :fo­cus-ring pseudo class – there may be a much simpler solution to this problem – (see the article)
    For what it’s worth, there is a polyfill for :focus-ring – see this article by Chris DeMars


A pragmatic alternative to keyboard-only focus styles

So achieving keyboard-only focus styles is surprisingly difficult. One alternative / workaround which is much simpler and may both fulfil the designer’s expectations and also be accessible – would be to style focus just like you would style for hover.

Codepen

So although technically this is not implementing keyboard-only styles, it essentially removes the need for keyboard-only styles.

Leave a Comment