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

Miro picture Miro · Jul 14, 2015 · Viewed 69.3k times · Source

I want to disable :focus when it's not needed because I don't like how my navigation looks when the focus is on it. It uses the same style as .active and it's confusing. However I don't want to get rid of it for people who use keyboard.

I was thinking to add a class enabled-focus on the body on tab press and then have body.enabled-focus a:focus{...} but that would add a lot of extra CSS for every element that has focus. Then remove that class from the body on first mouse down.

How would I go about it? Is there a better solution?

Answer

Danield picture Danield · Jul 19, 2017

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>

2) 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.

3) Remove default focus styling from both outer and inner elements:

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

4) 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

2) 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)

3) 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.