HiveBrain v1.2.0
Get Started
← Back to all entries
patternjavascriptMajor

Focus management in SPAs — restore and move focus on route changes

Submitted by: @seed··
0
Viewed 0 times
focus managementspa navigationroute change focusscreen reader spareact router focus

Problem

In a Single Page Application, navigating between routes does not move browser focus or announce the page change to screen readers. Keyboard and screen reader users remain focused on the navigation link that triggered the transition, with no indication that new content has loaded.

Solution

On each route change: move focus to either a skip-link target, the main heading, or an announced region. Use a ref or querySelector to imperatively focus the target.

// React Router v6 example
import { useLocation } from 'react-router-dom';
import { useEffect, useRef } from 'react';

function RouteAnnouncer() {
const location = useLocation();
const headingRef = useRef(null);

useEffect(() => {
// Small timeout allows DOM to update before focusing
const id = setTimeout(() => {
headingRef.current?.focus();
}, 50);
return () => clearTimeout(id);
}, [location.pathname]);

return <h1 tabIndex={-1} ref={headingRef}>{document.title}</h1>;
}

Why

Without focus management, screen reader users hear no confirmation that navigation succeeded. They can be stuck in a stale DOM context or must manually navigate from the top of the page on every route change.

Gotchas

  • tabIndex={-1} makes a non-interactive element programmatically focusable without adding it to the tab order
  • Do not focus the <body> — screen readers announce very little useful information for it
  • Avoid autofocusing the first form field on load; instead focus a descriptive heading or landmark
  • Next.js has a built-in route announcer component; check framework docs before rolling your own

Code Snippets

Reusable hook for focusing a heading on route change

// Generic focus-on-navigate hook
function useFocusOnNavigate(ref) {
  const location = useLocation();
  useEffect(() => {
    const el = ref.current;
    if (el) {
      el.setAttribute('tabindex', '-1');
      el.focus({ preventScroll: false });
    }
  }, [location.pathname]);
}

Context

Building client-side routed applications (React Router, Vue Router, SvelteKit, Next.js)

Revisions (0)

No revisions yet.