React renders content but never moves focus for you. When a modal opens or a route changes, keyboard and screen reader users can end up stranded on a hidden button. This article covers focus on open, focus on close, a simple tab trap, and route-change focus. Each step uses a ref plus an Effect, because focus is an imperative browser action that belongs outside render.
Focus the dialog when it opens
Attach a ref to the dialog container and focus it in an Effect. The container needs tabIndex of -1 so it can receive focus.
import { useRef, useEffect } from "react";
function Modal({ title, children }) {
const dialogRef = useRef(null);
useEffect(() => {
dialogRef.current.focus();
}, []);The Effect runs after commit and moves focus onto the dialog container. Without tabIndex, a plain div cannot be focused.
return (
<div role="dialog" aria-modal="true" aria-label={title} ref={dialogRef} tabIndex={-1}>
{children}
</div>
);
}When the modal opens, keyboard focus lands inside it instead of staying on the button behind it. A screen reader announces the dialog label at the same time. The Effect runs once on mount, so focus moves only when the modal first appears. For the simpler case of focusing a single input on demand, see how to focus an input with useRef.
Return focus when it closes
Save the element that had focus before opening, then hand focus back in the cleanup.
useEffect(() => {
const previous = document.activeElement;
dialogRef.current.focus();
return () => previous?.focus();
}, []);The optional chaining handles the case where the previously focused element no longer exists. On unmount, focus returns to the button that opened the modal, which matches the WAI-ARIA dialog pattern. If you skip this step, keyboard users close the dialog and land back at the top of the page, which is disorienting.
Keep Tab inside the dialog
A modal should keep its tab sequence contained. Intercept Tab and Shift+Tab in a keydown handler on the container.
function handleKeyDown(event) {
if (event.key !== "Tab") return;
const nodes = dialogRef.current.querySelectorAll("button, input, [tabindex]");
const first = nodes[0];
const last = nodes[nodes.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}This moves focus from the last element back to the first, and from the first back to the last when Shift is held. It keeps keyboard users inside the dialog until it closes. A full focus trap also handles Escape and elements added later, but this loop covers the common case.
Focus the heading on route change
After navigating, move focus to the page heading so assistive technology starts reading at the new content.
import { useRef, useEffect } from "react";
function ArticlePage() {
const headingRef = useRef(null);
useEffect(() => {
headingRef.current.focus();
}, []);
return <h1 ref={headingRef} tabIndex={-1}>Article title</h1>;
}The heading receives focus when the page mounts. The tabIndex of -1 makes it focusable without adding it to the normal tab order. Focusing the heading instead of the document body keeps screen reader users oriented in the new page.
What to learn next
Focus and announcements work together. Continue with how to announce dynamic updates to screen readers.
Rune AI
Key Insights
- Focus the dialog container in an Effect when the modal opens.
- Give the container tabIndex of -1 so it can receive focus.
- Save document.activeElement and restore it in cleanup.
- Trap Tab and Shift+Tab inside an open modal.
- Focus the page heading after each route change.
Frequently Asked Questions
Where should focus go when a modal opens?
How do I restore focus after closing a modal?
How do I focus a heading after a route change?
Conclusion
Focus management is an Effect plus ref task. Move focus into a modal when it opens, restore it on close, keep Tab inside, and focus the heading on route changes.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.