How to Manage Focus in React Modals and Route Changes

Move keyboard focus into a modal when it opens, restore it on close, trap Tab inside, and focus the heading when a route changes.

7 min read

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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
  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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
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

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.
RunePowered by Rune AI

Frequently Asked Questions

Where should focus go when a modal opens?

Into the dialog, usually on its container or first focusable element. Give the container tabIndex of -1 so it can receive focus.

How do I restore focus after closing a modal?

Save the previously focused element before moving focus, then return focus to it in the Effect cleanup.

How do I focus a heading after a route change?

Attach a ref to the heading, give it tabIndex of -1, and focus it in an Effect that runs on navigation.

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.