Robin van der Vleuten

Trap focus in a React component

Some interactions need to move focus into a specific part of the page and keep it there. A modal dialog is the usual example. We can use focus-trap to trap focus inside a DOM node or React component.

Interactive applications often need overlays. When an overlay is active, the rest of the page is usually hidden visually and the user should only interact with the overlay. ARIA attributes help assistive technologies understand that, but they do not stop keyboard users from tabbing out of the active element.

For example, aria-modal="true" tells assistive technologies that the elements behind the current dialog are not available^1. It does not, by itself, keep keyboard focus inside the modal. For that, we need JavaScript.

Trap focus with vanilla JavaScript

Trapping focus while staying accessible is fragile work^2. The focus-trap package handles the tricky parts for us.

Suppose we have this modal markup:

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 on the page, activate the focus trap 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 has more options, including deactivating when the user clicks outside the component or presses ESC.

Trap focus with React

In React, use focus-trap-react. It is a thin wrapper around the original package.

Here is the same example as 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'))

The modal is wrapped with <FocusTrap />. When showModal is true, focus stays inside the trap's children.

Closing thoughts

Accessible modals are easy to get wrong, especially for people who navigate with a keyboard. The focus-trap package gives you a tested way to keep keyboard focus where it belongs.