How to fix the ugly focus ring and not break accessibility in React

header image

Creating beautiful, aesthetic designs while maintaining accessibility has always been a challenge in the frontend. One particular barrier is the dreaded “:focus” ring. It looks like this:

focus outline on a button

After clicking any button, the default styling in the browser displays a focus outline. This ugly outline easily mars a perfectly crafted interface.

A quick Stack Overflow search reveals an easy fix: just use a bit of CSS, outline: none; on the affected element. It turns out that many websites use this trick to make their sites beautiful and avoid the focus outline.

Why you should never use outline: none

One word: accessibility. Users who cannot use a mouse and need to navigate the web using their keyboard are entirely lost when you remove your element’s focus outline.

Imagine wanting to click on a button on a page, but not knowing at all which button you’re currently hovering over. In fact, you don’t have to imagine! Go ahead and implement the outline: none; hack, and then try to tab through your page to get to your element to select it.

Not so easy, right? You can’t tell which element is selected.

It’s already difficult enough to navigate the web with a keyboard when sites are fully accessible. Removing accessible functionality from your site for the sake of design makes it impossible for users who cannot or prefer not to use a mouse to navigate your site.

Now that we’ve gained some empathy for those who with accessible needs, we can go ahead and put our focus outline back in. We can talk to our product designer about why the design won’t look perfect and call this a closed case.

But wait! There’s a way to please your design instincts and to allow users with accessible needs to navigate your page properly.

Previous solutions

Previous non-hacky solutions to this problem involve using the outline: none; trick but also add an event listener to listen for tabbing events, signifying a keyboard user. At that point, we add a new class to the document that adds the outline back in. This way, keyboard users still see the outline, but regular click users do not.

That sounds great, but generally, we don’t want to modify the document directly when writing React code. What else can we do?

Another approach involves calling onBlur after the event is handled. However, this doesn’t always work, as it can cause actions to be fired more than once accidentally. For instance, when using redux-form, if you call onBlur after the onClick action, you would see two submit actions fired: once on the first onClick, and again on the onBlur. Not what we’re expecting.

A third solution abandons the outline altogether in favor of a box-shadow instead. However, your designer may not be happy even in this case, since the box-shadow shows not only for keyboard users but also for click users as well.

Furthermore, with any of these approaches, the solution is limited only to a given component. We need a solution that is easier to apply in multiple places. Luckily for us, we can do this easily in React!

How to remove the focus outline in React

Let’s use the first solution from above as inspiration to create a new React component. This component’s only responsibility is to manage whether a focus outline should be shown. The component adds an event listener to listen for tabbing events, just like in the first solution we found.

Here’s how it all comes together:

class AccessibleFocusOutline extends React.Component {

    state = {
        enableOutline: false,
    }

    componentDidMount() {
        window.addEventListener('keydown', this._handleKeydown);
    }

    _handleKeydown = (e) => {
        // Detect a keyboard user from a tab key press
        const isTabEvent = e.keyCode === 9;

        if (isTabEvent) {
            this.setState({enableOutline: true});
        }
    }

    render() {
        return (
            <span className={ this.state.enableOutline ? '' : 'no-outline-on-focus' } >
                {this.props.children}
            </span>
        );
    }
}

And the CSS for the no-outline-on-focus class looks like this:

.no-outline-on-focus button:focus,
.no-outline-on-focus a:focus {
    outline: none;

}

We add the class by default and remove it when a tabbing event is detected.

Pretty simple, right? This solution works well when we want to wrap a component directly with our new AccessibleFocusOutline component. However, this component only handles <a> and <button> tags. What if we want our accessible focus outline to apply to any element we pass?

A more extensive solution

In our first approach, we created a component that receives children elements and wraps them with the proper accessible styles. To make this more extensive, let’s instead create a component that receives an HTML tag as a prop and creates the element for you with the styles applied. This way, we can pass any element tag to the component, and the styles apply correctly. Instead of an AccessibleFocusOutline component, we end up with an AccessibleFocusOutlineElement component.

Here’s what that looks like:

import classNames from 'classnames';
import omit from 'lodash/omit';

class AccessibleFocusOutlineElement extends React.Component {

    state = {
        enableOutline: false,
    }

    componentDidMount() {
        window.addEventListener('keydown', this._handleKeydown);
    }

    _handleKeydown = (e) => {
        const isTabEvent = e.keyCode === 9;

        if (isTabEvent) {
            this.setState({enableOutline: true});
        }
    }

    render() {
        const {className, tag: Tag} = this.props;

        // Omit the tag prop since we’ll use that to create
        // the element and we don’t want to forward it.
        // Omit the className prop as we’re going to combine 
        // it with our own class to control the outline.
        const forwardedProps = omit(this.props, ['tag', 'className']);

        const classes = classNames(
            className,
            {'no-outline-on-focus': !this.state.enableOutline},
        );

        return (
            <Tag className={classes} {...forwardedProps}>
                {this.props.children}
            </Tag>
        );
    }
}

So in this case, our CSS is a little simpler and does not need to look for particular HTML children elements, but rather applies directly to the element being passed to our component:

.no-outline-on-focus:focus {
    outline: none;
}

The result

Here’s the interaction for mouse users: no focus outline for regular click users!

no focus outline for click users

Moreover, for users navigating the site with a keyboard, the focus outline appears as we expect:

navigating buttons with focus outline

Don’t take shortcuts with design and accessibility

It can be tempting to use the first Stack Overflow result that works to achieve a beautiful design, like the outline: none; hack. However, the few minutes that you save implementing the feature could prevent users from navigating your site. Spend a few extra minutes to think about what a user with accessible needs might experience on your site and consider testing your page by only navigating with a keyboard.

While great design is always a priority, accessibility should be equally as important. I hope this component example assists you in making your sites more accessible without having to compromise on design.

Do you have another approach to solve the dreaded focus ring? Let us know in the comments or tweet at me @snazbala!

Article edited for clarity that this solution accounts for keyboard users only.

Photo by Elena Taranenko on Unsplash.

Leave a Reply

Your email address will not be published. Required fields are marked *