// guide
Focus Management in React: Refs, Effects, and Route Changes
React swaps DOM in and out without a page load, and focus goes with it. This guide shows the React-specific patterns for keeping focus under control: moving focus with refs and useEffect, handling focus on route changes in single-page apps, trapping it in modals, and restoring it when they close.
// 01 · why focus needs managing in react
Why Focus Needs Managing in React
In a traditional multi-page site, the browser handles focus: every navigation loads a new document and focus resets to the top. React apps break that model. Views change, dialogs appear, and list items come and go — all without a page load — and whatever element had focus can be removed from the DOM underneath it. When that happens, focus falls back to <body>, and a keyboard or screen reader user is stranded with no idea where they are.
React deliberately does not manage focus for you, because only you know where it should go. This guide covers the four situations that matter most: moving focus on demand, handling client-side route changes, trapping focus in overlays, and restoring it afterward. It is the React-specific companion to the general focus management guide.
// 02 · moving focus with refs and effects
Moving Focus with Refs and Effects
The building block for all React focus management is a ref to the target element and a useEffect that focuses it after the DOM is committed. Doing it in an effect (not during render) guarantees the node exists when you call .focus().
function ErrorSummary({ errors }) {
const ref = useRef(null);
useEffect(() => {
if (errors.length > 0) {
ref.current?.focus();
}
}, [errors]);
if (errors.length === 0) return null;
return (
<div ref={ref} tabIndex={-1} role="alert">
<h2>{errors.length} problems need your attention</h2>
<ul>
{errors.map((e) => (
<li key={e.id}><a href={`#${e.field}`}>{e.message}</a></li>
))}
</ul>
</div>
);
}
Two details make this work. The dependency array ([errors]) re-runs the effect whenever the errors change, so focus moves each time a new set appears. And tabIndex={-1} lets a non-interactive container receive programmatic focus without adding it to the Tab order. This same shape — ref, effect, tabIndex={-1} — drives every pattern below.
// 03 · focus on route change
Focus on Route Change
Client-side routing is the most-missed focus problem. A router swaps the view without a page load, so a screen reader announces nothing and keyboard focus stays on the link the user just clicked. The fix: on every navigation, move focus to the new view's main heading and update the document title.
import { useLocation } from 'react-router-dom';
function Page({ title, children }) {
const { pathname } = useLocation();
const headingRef = useRef(null);
useEffect(() => {
document.title = `${title} — My App`; // announce the new page name
headingRef.current?.focus(); // move focus into the new view
}, [pathname, title]);
return (
<>
<h1 tabIndex={-1} ref={headingRef}>{title}</h1>
{children}
</>
);
}
Focusing the <h1> does double duty: it announces the heading text (telling screen reader users what page they are on) and it puts keyboard users at the top of the new content, so their next Tab starts there rather than back in the old view. An alternative is to focus the <main> landmark; either works, as long as focus moves somewhere meaningful. See the SPA accessibility guide for the broader single-page-app picture.
// 04 · trapping focus in modals
Trapping Focus in Modals
A modal dialog needs the full focus lifecycle: move focus in when it opens, keep Tab cycling inside it while open, close on Escape, and return focus to the trigger when it closes. The open-and-restore halves are pure ref-and-effect work.
function Dialog({ onClose, children }) {
const dialogRef = useRef(null);
useEffect(() => {
// Remember what had focus before the dialog opened
const previouslyFocused = document.activeElement;
// Move focus into the dialog
dialogRef.current?.focus();
// On close/unmount, return focus to the trigger
return () => {
if (previouslyFocused instanceof HTMLElement) {
previouslyFocused.focus();
}
};
}, []);
return (
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
tabIndex={-1}
onKeyDown={(e) => e.key === 'Escape' && onClose()}
>
{children}
</div>
);
}
The effect's cleanup function is the key: capturing document.activeElement when the dialog mounts and calling .focus() on it when the dialog unmounts gives you correct focus restoration for free. The one piece not shown is the Tab trap — keeping focus cycling within the dialog — which is fiddly to get right by hand. The native <dialog> element traps focus for you, and well-tested libraries handle it too; either is preferable to a hand-rolled trap. Follow the modal pattern for the complete behavior spec.
<dialog> element (opened with showModal()) provides the focus trap, Escape-to-close, and backdrop for free. In React you still manage where focus lands, but you inherit the hard parts. Reach for it before building a trap from scratch.
// 05 · common mistakes
Common Mistakes
- No focus handling on route change. The most common SPA failure — focus stays on the clicked link and the new page is never announced. Move focus to the heading.
- Focusing during render or with
querySelector. The node may not exist yet, and you fight React's lifecycle. Use a ref insideuseEffect. - Not restoring focus after a modal closes. Focus drops to the top of the page. Capture
document.activeElementon open and restore it on close. - Forgetting
tabIndex={-1}on non-interactive targets. You cannot programmatically focus a plain<h1>or<div>without it. - Trapping focus but never releasing it. A trap that outlives the modal locks the whole page. Tie the trap strictly to the open state.
- Auto-focusing something far down the page on load. Scrolls the user past content unexpectedly. Only move focus in response to a real interaction.
Frequently asked questions
How do I move focus to an element in React?
Attach a ref to the element and call .focus() on it inside a useEffect, so the move happens after React has committed the DOM: const ref = useRef(null); useEffect(() => { ref.current?.focus(); }, []);. Never query the DOM with document.querySelector and focus it during render — the node may not exist yet, and you are fighting React's lifecycle. Refs plus effects are the supported way.
Where should focus go after a route change in a single-page app?
Because a client-side navigation does not reload the page, screen reader users get no signal that the view changed and keyboard focus is left wherever it was. On each route change, move focus to a sensible landmark in the new view — usually the main heading (<h1>) or the main region, given a tabIndex={-1} so it can receive programmatic focus. This announces the new page and puts keyboard users at its start.
How do I trap focus inside a React modal?
When the dialog opens, move focus into it (to the first control or the dialog container), keep Tab and Shift+Tab cycling within it while it is open, close it on Escape, and return focus to the trigger on close. You can implement this with refs and key handlers, but a well-tested library or the native <dialog> element saves considerable effort. Whatever you use, follow the modal pattern for the exact behavior.
How do I restore focus when a modal or menu closes?
Capture the currently focused element before you open the overlay — const trigger = document.activeElement — then call trigger.focus() after it closes. In React, store that reference in a ref when the overlay mounts and restore it in the cleanup of the same useEffect, or in the close handler. Without this, focus falls back to the top of the document and keyboard users lose their place entirely.
Why not just autofocus with the autoFocus prop?
The autoFocus prop works for a simple case like focusing the first field of a form that is present on mount, and it is fine there. But it does not help with focus that must move in response to an interaction — opening a dialog, changing routes, revealing an error summary — because the element may already be mounted, or may mount later. For those, an explicit ref-and-effect (or a focus call in an event handler) gives you control over exactly when focus moves.