CSS Focus Styles for Keyboard Users Only

There is a fine line to be trodden between the aesthetic expectations of a project, and allowing suitable accessibility. I would always argue that accessibility far outweighs any importance visuals carries, but that can be an uphill struggle in teams where the distinction between technical UX and UI aesthetics has become blurred.
We are all familiar with the situation where interactive elements on‑page gain an unexpected blue glow:
- Text inputs when clicked into;
- Links and buttons when clicked upon.
It is these things that will have the hybrid UX/UI person frenetically walking up behind you to push their laptop in your face and frantically point at it.

This is inherited from the :focus pseudo‑class, jump onto Stack Overflow and you will find dozens of answers all saying the same thing: remove the outline, maybe even drop an !important in there for good measure.
*:focus {
outline: none !important;
}The key thing to bear in mind is that this bright outline is very important for keyboard or visually impaired users. I won't dwell on this point for too long because I hope to make it absolutely clear in a single, short sentence:
You must never just remove CSS outlines.
If you find yourself struggling against this one in a professional development environment, give them my email address. I'll explain it to them.
To placate both visuals and accessibility, there are three options you can explore:
- Style the outline. It doesn't actually have to be that default blue glow (which differs from browser to browser). As long as it is visually distinctive from the surrounding site, keyboard users will still be able to use it.
- Give the element more specific styling. You really should have a defined visual state for
:focusanyway, just make sure it is distinctive enough to show that the interaction is occurring. - Take the outlines away, but only for non‑keyboard users.
It is a combination of the second and third options that I most commonly implement. As I mentioned: you should already have a distinct style for your interactables anyway, remember the minimum you should be styling is default, :visited, :hover, :active, :focus. It often makes sense to combine these so that default and visited are the same, as are the other three:

So, with obvious enough visuals, you could argue that an outline isn't necessary at all.
Take Away Outlines for Non‑Keyboard Users Only
This is where things get a little more interesting. Although it should be a last resort, it is also possible to remove outlines for non‑keyboard users, whilst retaining them for people traversing and interacting with the site via the keyboard.
:focus-visible uses the browser's judgement about when focus needs to be visible, rather than simply detecting a mouse or keyboard. A text input can still need an indicator after a click. Here's an example from the MDN documentation which I've modified a little and moved into Sass using the parent selector:
.button {
display: block;
// Provide a fallback style for browsers
// that don't support :focus-visible
&:focus {
outline: none;
background: lightgrey;
// Remove the focus indicator on mouse-focus for browsers
// that do support :focus-visible
&:not(:focus-visible) {
background: transparent;
}
}
// Draw a very noticeable focus style for keyboard-focus
// on browsers that do support :focus-visible
&:focus-visible {
outline: 4px dashed darkorange;
background: transparent;
}
}As of September 2020, browser support is still limited, and whilst there is a polyfill available, I used the narrower workaround below for this site.

For this workaround, call useKeyboardFocus once in a component near the root of the application that stays mounted between pages. The document access and listeners belong inside the effect, which runs in the browser:
import { useEffect } from 'react';
const useKeyboardFocus = () => {
useEffect(() => {
const bodyEl = document.body;
const keyboardClass = 'keyboard-user';
const hadKeyboardClass = bodyEl.classList.contains(keyboardClass);
const handleKeydownOnce = (event) => {
if (event.key !== 'Tab') return;
bodyEl.classList.add(keyboardClass);
document.removeEventListener('keydown', handleKeydownOnce);
document.addEventListener('mousedown', handleMousedownOnce);
};
const handleMousedownOnce = () => {
bodyEl.classList.remove(keyboardClass);
document.removeEventListener('mousedown', handleMousedownOnce);
document.addEventListener('keydown', handleKeydownOnce);
};
document.addEventListener('keydown', handleKeydownOnce);
return () => {
document.removeEventListener('keydown', handleKeydownOnce);
document.removeEventListener('mousedown', handleMousedownOnce);
bodyEl.classList.toggle(keyboardClass, hadKeyboardClass);
};
}, []);
};
export default useKeyboardFocus;What this does is:
- Use the React Effect Hook to add a
keydownevent listener. On unmount, remove both possible listeners and restore the previous body class. You could just as easily use vanilla JavaScript to achieve this if you aren't using React. - When a key event occurs, we check
event.keyto determine whether it was the Tab key that was pressed or not. In the past, we could have checked to see ifevent.keyCodeis9(the tab key), butkeyCodehas been deprecated for some time. That does not tell us when browsers might remove it. Check support forKeyboardEvent.key, which may be incomplete in older browsers so if you are supporting legacy Firefox or Internet Explorer, you may need to use a combination ofkeyand/orcodealongsidekeyCode. - If it was a tab key, we know this is somebody attempting to traverse the page using the keyboard. Set the classname '
keyboard-user' onbody, remove the keyboard event listener, and instead attach an event to listen for mouse events. - If there is a mouse event, we know this user is now using the mouse instead. We remove the classname from the
body, and reinstate the keyboard listener.
This tracks Tab presses and mouse presses for the workaround; it is not a complete account of how someone interacts with the page. Keep the wider limitations in mind, particularly text inputs and focus moved by scripts.
The final piece of the puzzle is a simple piece of CSS (or Sass):
body {
&:not(.keyboard-user) {
*:focus,
*:active {
outline: none;
}
}
}This effectively removes the outline for non‑keyboard users and allows the browser default interaction highlight (assuming it has not been overridden elsewhere in your project) when using the keyboard. This describes the approach I used on this site in September 2020.
As a final note on the subject: it should be considered that outlines aren't only indicative of, or for, keyboard users: the highlight allows for much easier passage through your site. Unless you are able to implement really clear visual indicators for your interactables, I would always advise great caution in overriding browser defaults.
Postscript
March 2022: Native :focus-visible is now available across the major browsers. I'd prefer it to the event‑listener workaround when it covers the browsers you support. The browser uses heuristics to decide when focus needs an indicator, so this is not a strict mouse‑versus‑keyboard switch: text inputs can match after pointer focus too. Retain a visible :focus fallback for browsers without support.