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

CssFocus

Css Problem Overview


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?

Css Solutions


Solution 1 - Css

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>

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.

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

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

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

Solution 2 - Css

Case study: Facebook login page

Facebook is using a tiny bit of Javascript on their login page right now (June 2018).

The Javascript detects when the user has clicked their mouse or used their keyboard, and toggles a class on and off on the body: <body class="using-mouse">

Then CSS rules can use that class to show or hide the appropriate focus styling on the relevant elements.

Here is some example code (also available on CodePen). Compare clicking and tabbing.

// Let the document know when the mouse is being used
document.body.addEventListener('mousedown', function() {
  document.body.classList.add('using-mouse');
});

// Re-enable focus styling when Tab is pressed
document.body.addEventListener('keydown', function(event) {
  if (event.keyCode === 9) {
    document.body.classList.remove('using-mouse');
  }
});

// Alternatively, re-enable focus styling when any key is pressed
//document.body.addEventListener('keydown', function() {
//  document.body.classList.remove('using-mouse');
//});

/* The default outline styling, for greatest accessibility. */
/* You can skip this to just use the browser's defaults. */
:focus {
  outline: #08f auto 2px;
}

/* When mouse is detected, ALL focused elements have outline removed. */
body.using-mouse :focus {
  outline: none;
}

<input>
<button>Submit</button>

Note that :focus above is equivalent to *:focus, matching all elements. If you only wanted to remove styling from buttons, you could put button:focus there instead.


Case study: GMail login page

Alternatively, at that time GMail was just styling focused buttons with a heavier shadow than unfocused buttons, regardless of whether the user was on mouse or keyboard.

This is simple to implement and understand, and doesn't require any Javascript.

:focus {
  outline: none;
  box-shadow: 0 0px 16px #0005;
}

But it's a compromise. It conveys focus information that mouse users aren't really interested in, and it might be a bit too subtle for keyboard users.

Still, this compromise is probably better than either one of the extremes (a strong outline for all users, or no outline at all).


StackOverflow's primary buttons use a similar approach to GMail, but with a more stylised look:

box-shadow: inset 0 1px 0 0 rgba(102,191,255,0.5), 0 0 0 4px rgba(0,149,255,0.15);

Personally I would use a stronger (higher contrast) colour, for accessibility.

Solution 3 - Css

Removing outline is terrible for accessibility! Ideally, the focus ring shows up only when the user intends to use the keyboard.

2018 Answer: Use :focus-visible. It's currently a W3C proposal for styling keyboard-only focus using CSS. Until major browsers support it, you can use this robust polyfill. It doesn't require adding extra elements or altering the tabindex.

/* Remove outline for non-keyboard :focus */
*:focus:not(.focus-visible) {
  outline: none;
}

/* Optional: Customize .focus-visible */
.focus-visible {
  outline-color: lightgreen;
}

I also wrote a more detailed post just in case you need more info.

Solution 4 - Css

This is a problem you will probably encounter a lot. The good thing about such problems is, if you once find a solution, it won't bother you any more.

The most elegant solution seems to be the simplest: don’t remove the outline on :focus, do it on :active instead – after all, :active is the dynamic pseudo-class that deals explicitly with the styles that should be applied when a focusable element is clicked or otherwise activated.

a:hover, a:active { outline: none; }

The only minor issues with this method: if a user activates a link and then uses the browser’s back button, the outline becomes visible. Oh, and old versions of Internet Explorer notoriously get confused by the exact meaning of :focus, :hover and :active, so this method fails in IE6 and below.

Tip

There is a trivial workaround to prevent outlines from “spilling over” by adding a simple overflow:hidden, which keeps the outline in check around the clickable portion of the element itself.

Solution 5 - Css

UPDATE 2020

:focus-visible has landed in stable Chrome. Just use it already! Only IE and Safari support is missing, so add a fallback (below).

Need a nice feature query to distinguish between IE11 + Safari and the rest? Here's an SCSS mixin:

@mixin focus-visible-fallback {
  @supports (contain: none) {
    &:focus-visible {
      outline: none;
      @content;
    }
  }

  /* Safari & IE11 */
  @supports not (contain: none) {
    &:focus {
      outline: none;
      @content;
    }
  }
}

Note: as mentioned in the comments, <input> will always get focus regardless if :focus or :focus-visible is used.

ORIGINAL POST

Until :focus-visible is not there in all popular evergreen browsers, you can use this simple trick in the global part of your CSS, without any polyfills:

@media (pointer: coarse) {
  *:focus {
    outline: none;
  }
}

and then add focus effects as you normally do, with :focus.

At this point you probably learned, that setting the outline: none to focused elements by default is a horrible idea from the accessibility point of view. That is certainly true.

However, if you scope this rule in the pointer: coarse media query, it becomes very useful, since it will only apply to mobile phones and tablets, but not desktops. Which is exactly what you want to achieve.

The only issue I can think about are mobile users with keyboards, which they use for tabbing through content, but I'm not sure if there is a lot of such users. So, ultimately :focus-visible will be a better solution, but for now this should be enough.

Solution 6 - Css

&:focus:not(:hover) { }

It won't work in 100% of cases but I think for most people's need this should be sufficient.

It will prevent the :focus state being trigger on click because the mouse has to be over (hovering) the element to click it.

https://codepen.io/heyvian/pen/eopOxr

Solution 7 - Css

In playing with the accepted solution by Danield, I found an alternative, simpler way based on the inner/outer div concept.

  1. Create an outer and inner element. Give the outer element tabindex="0" and the inner element tabindex="-1"

    I'm a button!

  2. In the css, remove outline from the inner element when focused:

    .inner:focus{ outline: none; }

  3. Apply any mouse or click event handlers to the inner element. Apply any focus events (onfocus, onblur, onkeydown) to the outer element.

For example:

<div role="button" class="outer" tabindex="0" onfocus="focusEventHandler()" onkeydown="handleKeyDown.bind(this, myEventHandler)">
    <div class="inner" tabindex="-1" onClick="myEventHandler()">
        I'm a button!
    </div>
</div>

**Maintain the size and positioning such that the inner element completely overlaps the outer element. Position the entire "button" with styling on the outer element.

How this works:

When the user clicks on the "button", they are clicking on the inner element which has the focus outline removed. It is not possible to click on the outer element since it is covered by the inner element. When the user uses the keyboard to tab to the "button", they get to the outer element (tabindex="0" makes the element reachable with 'tab') which gets a focus outline, but the inner element is not reachable through the tab (with tabindex="-1") and does not receive focus outline when clicked.

Solution 8 - Css

As several has stated, :focus-visible is the way to go in terms of a pure CSS solution. I want to provide the easiest way I've solved this styling issue using CSS only, however it has some drawbacks in browser support and is not useful for everyone:

@supports not selector(:focus-visible) {
    :focus {
        // Fallback styles for browsers that doesn't support :focus-visible.
    }
}

:focus-visible {
    // Tab focus styles
}

Only apply :focus in case it's really needed to prevent interference with :focus-visible styles. :focus-visible will simply be ignored by browsers not supporting it and therefore need no @supports or such.

Read more about the browser support for @supports selector and :focus-visible.

In case you happen to use CSS in JS like I do, you can utilize CSS.supports() to conditionally render the fallback CSS in case you need to care about browsers that doesn't support @supports selector. Something along the lines of if (CSS.supports("selector(:focus-visible)")) {...}.

Solution 9 - Css

There is no clear solution. I have done one Hackish solution : apply click event on your Main Container and write below code on click

	_handleMouseClick = (event) => {
		if(event.detail){
			document.activeElement.blur();
		}
	}

When you click using mouse you will get event.detail = 1 on that click blur that element so that it will remove the outline and on keyboard click we get event.detail = 0 so in keyboard case behave normal

OR

In css file

	 body.disableOutline *:focus{
		outline: none !important;
	}

In Main js

	 document.addEventListener('click', _handleMouseClick,true);
			document.addEventListener('keydown',_keydown,true);
			function _handleMouseClick(event){
				if(event.detail){
					document.getElementsByTagName("body")[0].classList.add("disableOutline");
				}
			}
			function _keydown(e){
				document.getElementsByTagName("body")[0].classList.remove("disableOutline");
			}

Solution 10 - Css

blueprintjs library has a nice solution for this.

https://blueprintjs.com/docs/#core/accessibility

However, I could not understand how they managed it yet.

Solution 11 - Css

As it is mentioned by others there is the :focus-visible option with quite good browser support https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible.

I found very useful this article and wanted to share it https://css-tricks.com/keyboard-only-focus-styles/

Example in SCSS:

button {

 &:focus-visible {
    border: 2px solid #004EA3;
  }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionMiroView Question on Stackoverflow
Solution 1 - CssDanieldView Answer on Stackoverflow
Solution 2 - CssjoeytwiddleView Answer on Stackoverflow
Solution 3 - CssAaron Noel De LeonView Answer on Stackoverflow
Solution 4 - CssJeremy ZahnerView Answer on Stackoverflow
Solution 5 - CssNeurotransmitterView Answer on Stackoverflow
Solution 6 - CssVian EsterhuizenView Answer on Stackoverflow
Solution 7 - CssnutsandboltsView Answer on Stackoverflow
Solution 8 - Cssmikaelkarlsson-seView Answer on Stackoverflow
Solution 9 - CsspareshmView Answer on Stackoverflow
Solution 10 - Cssfunky-ndView Answer on Stackoverflow
Solution 11 - CssDrNioView Answer on Stackoverflow