Trap focus in a React component
Tags: react, javascript, webdev, a11y
Published: 2020-10-12 14:39:42

Some actions on a web application requires shifting the focus of an user. We'll use the [focus-trap](https://github.com/focus-trap/focus-trap) package to trap focus within a DOM node or React component.

While developing interactive applications for the modern web, changes are that you'll need to create overlaying elements. When such elements are active, the rest of the page is usually visually hidden and the user is only allowed to interact with this element. We can use ARIA-attributes to help with assistive technologies, but that won't prevent keyboard users from tabbing out of the active element.

For example, setting `aria-modal="true"` on an active modal will tell assistive technologies that the elements below the current dialog are not available to interact with[^1]. This, however, will not prevent the user from tabbing out of your modal component. In this case we’ll have to resort to JavaScript and trap the keyboard focus, and keep the tabbed element in our modal overlay.

## Trap focus with vanilla JavaScript

Trapping focus and staying accessible is quite complex and fragile functionality[^2]. So we're going to use the well written [focus-trap](https://github.com/focus-trap/focus-trap) to help implementing an accessible modal component.

Let's say that we have the following HTML markup containing the modal.

```html
<div id="demo">
    <button id="show">show modal</button>

    <div id="modal">
        Modal with <a href="#">with</a> <a href="#">some</a>
        <a href="#">focusable</a> elements.

        <button id="hide">hide modal</button>
    </div>
</div>
```

With a script attached to the HTML page, we can trap the focus when the modal is shown.

```js
import { createFocusTrap } from 'focus-trap'

const modal = document.getElementById('modal')

const focusTrap = createFocusTrap('#modal', {
    onActivate: function () {
        modal.className = 'trap is-visible'
    },
    onDeactivate: function () {
        modal.className = 'trap'
    },
})

document.getElementById('show').addEventListener('click', function () {
    focusTrap.activate()
})

document.getElementById('hide').addEventListener('click', function () {
    focusTrap.deactivate()
})
```

The `focus-trap` package provides many more options. Like deactivating when clicking outside of the component or trigger deactivating when the `ESC` key is pressed.

## Trap focus with React

To achieve the focus trap with React, we can use the [focus-trap-react](https://github.com/focus-trap/focus-trap-react) package. It is a thin wrapper around the original package.

Let's convert our vanilla JavaScript example to a React component.

```jsx
import React from 'react'
import ReactDOM from 'react-dom'
import FocusTrap from 'focus-trap-react'

const Demo = () => {
    const [showModal, setShowModal] = React.useState(false)

    return (
        <div>
            <button onClick={() => setShowModal(true)}>show modal</button>

            <FocusTrap active={showModal}>
                <div id="modal">
                    Modal with <a href="#">with</a> <a href="#">some</a>{' '}
                    <a href="#">focusable</a> elements.
                    <button onClick={() => setShowModal(false)}>
                        hide modal
                    </button>
                </div>
            </FocusTrap>
        </div>
    )
}

ReactDOM.render(<Demo />, document.getElementById('demo'))
```

You'll notice that we've wrapped the modal with an `<FocusTrap />` component. Whenever the trap is active - as indicated by the `showModal` state - only the children within the trap will get focus.

## Closing thoughts

It is quite complex to develop a modal or a dialog component that is accessible to any of your users. Especially when they are visually impaired and depend on the keyboard to navigate through your application. By adding the [focus-trap](https://github.com/focus-trap/focus-trap) package to your toolbelt, it becomes much easier to create keyboard-navigable components without leaving users behind.

[^1]: https://www.w3.org/TR/wai-aria-practices/examples/dialog-modal/dialog.html
[^2]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/dialog_role#Required_JavaScript_features