How to Trap Focus in Next.js and React
Trapped Focus Example
Published on: (Updated on: )
In web development, managing focus is critical for accessibility, especially when creating modal dialogs, pop-ups, or interactive components. Trapping focus ensures that keyboard users can navigate your component without losing focus elsewhere on the page. In this blog post, we'll explore how to implement a simple focus trap using React, specifically in a Next.js environment, with a thorough breakdown of the code.
What is Focus Trapping?
Focus trapping is a technique that restricts keyboard navigation to a specific part of the UI, preventing users from accidentally interacting with elements outside of this section. This is particularly useful in modal dialogs, dropdowns, and other interactive components where users need to focus on specific actions or information.
Setting Up Your Next.js Project
Before we dive into coding, ensure you have a Next.js project set up. If you haven’t created one yet, you can quickly start by running:
npx create-next-app@latest my-focus-trap-app
cd my-focus-trap-app
Once your project is ready, navigate to the directory and create a new component called FocusTrap.tsx.
Implementing the Focus Trap Component
Now, let’s examine the FocusTrap component. This component takes two props: children (the content to be rendered) and active (a boolean to control if the focus trap is active).
Here's the complete focus trap code:
import React, { useEffect, useRef } from "react";
interface FocusTrapProps {
children: React.ReactNode;
active: boolean; // Control when the focus trap is active
}
const FocusTrap: React.FC<FocusTrapProps> = ({ children, active }) => {
const containerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!active || !containerRef.current) return;
const focusableElements = 'button, [href], [tabindex]:not([tabindex="-1"])';
const focusableArray = Array.from(containerRef.current.querySelectorAll<HTMLElement>(focusableElements));
const firstFocusableElement = focusableArray[0];
const lastFocusableElement = focusableArray[focusableArray.length - 1];
const handleKeyDown = (e: KeyboardEvent): void => {
if (e.key === "Tab") {
if (e.shiftKey) { // Shift + Tab
if (document.activeElement === firstFocusableElement) {
e.preventDefault();
lastFocusableElement.focus();
}
} else { // Tab
if (document.activeElement === lastFocusableElement) {
e.preventDefault();
firstFocusableElement.focus();
}
}
}
};
containerRef.current.addEventListener("keydown", handleKeyDown);
// Focus the first element inside when the trap is active
firstFocusableElement?.focus();
return () => {
containerRef.current?.removeEventListener("keydown", handleKeyDown);
};
}, [active]);
return (
<div ref={containerRef} className="fixed top-0 left-0 w-full h-full flex justify-center items-center backdrop-blur-sm z-10">
{children}
</div>
);
};
export default FocusTrap;
Code Explanation
Imports and Interfaces: We import React along with useEffect and useRef to handle the lifecycle and DOM references within our component. We define our props with TypeScript for type safety.
Using useRef: containerRef provides a reference to the DOM element that contains our children. This is crucial for querying focusable elements.
Effect Hook: We use useEffect to set up our focus trapping logic when the active state changes.
Focusable Elements: We define a string that tells the component which elements should be focusable (button, links, and elements with a tabindex).
Handle Key Down: The handleKeyDown function captures key events. If the "Tab" key is pressed, it checks if the focus is on the first or the last focusable element:
- If on the first and "Shift + Tab" is pressed, focus shifts to the last focusable element.
- If on the last and just "Tab" is pressed, focus shifts back to the first focusable element.
Cleanup: We ensure to remove the event listener on cleanup to prevent memory leaks.
Focus Control: The first focusable element gets focused whenever the trap becomes active.
Using the FocusTrap Component
You can now use the FocusTrap component in various parts of your application:
import React, { useState } from "react";
import FocusTrap from "./FocusTrap";
const ModalExample = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
{isOpen && (
<FocusTrap active={isOpen}>
<div className="modal-content">
<h2>Modal Title</h2>
<button onClick={() => setIsOpen(false)}>Close</button>
<button>Another Action</button>
</div>
</FocusTrap>
)}
</div>
);
};
export default ModalExample;
In this example, the ModalExample component allows you to open a modal with focus trapping enabled. When the modal is active, users can only tab through the buttons inside it.
Conclusion
Implementing a focus trap in Next.js and React is straightforward with the use of refs and the keydown event listener. It significantly enhances the accessibility of your application, ensuring keyboard users can navigate effectively. By following this guide, you've learned how to implement a simple yet effective focus trap using a functional React component.